- web6047 - (2021/09/10(金) 現在、システム調整中のため、一部の表示がおかしいかもしれません)

これは砂浜と波です↓

web6047 2021年

プログラミングやRPG(作るほう)が好きな人の日記



このウェブページは毎日 夜11時にアクセスできなくなります。

朝6時半に再開されます。(世の中のネット依存対策として)

例外でアクセスができる場合があります。上のメニューの「aboutThisWebsite」を参照してください。


以下の表は、このウェブページの管理人のパソコンの使用時間を管理・制限するためのものです。





































NO PC WEEK に代わる PC 使用制限のしくみ(新β版)
No. A1.
開始時
運動
A2.
勉強
1問
A3.
終了時
運動
H1. 予定
作業内容
H2. 予定
作業詳細(進捗%)
判定×の理由
B. 実際
開始時刻
C. 予定
使用時間
(当日限度)
D. 予定
終了時刻
E. 実際
終了時刻
F. 実際
使用時間
G. 判定
◎ 9分以下
○ 10分~19分
△ 20分~29分
× 30分以上
予定


記事をカテゴリ分けする カテゴリをさらに大きな大カテゴリで分ける





予定


記事をカテゴリ分けする 各記事の日付を表示





予定


3Dお姉さんプログラム説明 座標を持った2つの要素について片方がもう片方を追尾し続ける試作(この表のNo. 380の問題を受け)





431 中止 全体的・基本・簡潔のRPG コマンドメニュー作成(97.9%)
もし時間内に終わらない場合は:あきらめる
18:10 2:00(2:00) 20:10 20:21 2:11
430 中止 中止 全体的・基本・簡潔のRPG コマンドメニュー作成(97.8%)
もし時間内に終わらない場合は:あきらめる
19:25 1:00(4:00) 20:25 20:31 1:06
429 中止 中止 全体的・基本・簡潔のRPG コマンドメニュー作成(97.7%)
もし時間内に終わらない場合は:あきらめる
16:25 1:30(4:00) 17:55 18:01 1:36
428 コロナ接種直後のため中止 中止 全体的・基本・簡潔のRPG コマンドメニュー作成(97.6%)
もし時間内に終わらない場合は:あきらめる
13:35 1:30(4:00) 15:05 15:14 1:39
427 全体的・基本・簡潔のRPG (コマンドメニュー作成は中断し、コマンドメニューの戦闘画面対応のために)
マップ上でのイベント発生のしくみ(戦闘エンカウントほか)(75%)
もし時間内に終わらない場合は:あきらめる
21:20 1:30(1:30) 22:50 22:50 1:30
426 全体的・基本・簡潔のRPG (コマンドメニュー作成は中断し、コマンドメニューの戦闘画面対応のために)
マップ上でのイベント発生のしくみ(戦闘エンカウントほか)(70%)
もし時間内に終わらない場合は:あきらめる
20:15 1:30(1:30) 21:45 22:03 1:48
425 全体的・基本・簡潔のRPG コマンドメニュー作成(97.5%)
×現状のRPGプログラムを動く状態で掲載しようとしたら、うまくいかなかった。。
もし時間内に終わらない場合は:あきらめる
14:30 1:00(2:00) 15:30 16:18 1:48 ×
424 全体的・基本・簡潔のRPG コマンドメニュー作成(97.4%)
もし時間内に終わらない場合は:あきらめる
17:04 1:30(UNLOCK) 18:34 18:43 1:39
423 全体的・基本・簡潔のRPG コマンドメニュー作成(97.3%)
もし時間内に終わらない場合は:あきらめる
14:20 1:30(UNLOCK) 15:50 16:01 1:41
422 全体的・基本・簡潔のRPG コマンドメニュー作成(97.2%)
もし時間内に終わらない場合は:あきらめる
15:53 2:00(UNLOCK) 17:53 18:03 2:10
421 全体的・基本・簡潔のRPG コマンドメニュー作成(97.1%)
もし時間内に終わらない場合は:あきらめる
13:20 1:30(UNLOCK) 14:50 15:00 1:40
420 全体的・基本・簡潔のRPG コマンドメニュー作成(97%)
もし時間内に終わらない場合は:あきらめる
10:20 1:30(UNLOCK) 11:50 11:59 1:39
419 全体的・基本・簡潔のRPG コマンドメニュー作成(96.9%)
もし時間内に終わらない場合は:あきらめる
16:52 1:30(4:00) 18:22 18:33 1:41
418 全体的・基本・簡潔のRPG コマンドメニュー作成(96.8%)
もし時間内に終わらない場合は:あきらめる
13:10 2:00(4:00) 15:10 15:25 2:15
417 × 全体的・基本・簡潔のRPG コマンドメニュー作成(96.7%)
もし時間内に終わらない場合は:あきらめる
22:40 1:30(1:30) 24:10 24:08 1:28
416 全体的・基本・簡潔のRPG コマンドメニュー作成(96.6%)
もし時間内に終わらない場合は:あきらめる
21:20 1:30(1:30) 22:50 22:52 1:32
記録しなかった長時間作業あり:
だめだこりゃ10:30~19:30
長時間になるおそれがあるのはわかっていたのに、それを承知で初めて、案の定、9時間となった。
作業内容は、HTMLの文字部分にCANVASの内容を描こうというもの。
いちおう できはしたけど、なんなんだろう。
それでも、この表による効果は日ごろから上がっていて、プラスの状態だとは思うけど。
415 全体的・基本・簡潔のRPG コマンドメニュー作成(96.5%)
もし時間内に終わらない場合は:あきらめる
21:20 3:00(UNLOCK) 24:20 24:33 3:13
記録しなかった長時間作業あり:
自作の便利プログラム「WebBasic」に代わる、JavaScriptの機能を試してしまった。23:30-翌朝5:30
・CANVASをページ上にに適切な大きさで拡大表示することができる(CSSのobject-fitプロパティ)
・HTML要素がユーザーの目に入ったかどうかを知ることができる「JavaScriptのIntersectionObserver」
・それらを使用した、プログラムの新しい掲載方法の模索、サンプルプログラム2点の作成と掲載。。。
結果的には、得るものを十分に得たんですが、体の調子は崩れました。
夜通しコンピューターの世界で試行錯誤を重ねて、最後に宝を得る。。これを私はひそかに「ディグダグした」と表現しています。(リンクはナムコの公式動画へ)
宝を見つけるまでの過酷な様子(夜更かし、疲れ、作業の困難さ)と、ディグダグという言葉のゴテゴテとした印象を重ねて。
ディグダグとなった分かれ道のポイントは、今回は「JavaScriptの機能をちょっと試したい」という欲望に自ら乗ったところからでした。
創作の幅を広げることになったので「善」と取りたい気持ちもありますが、翌日に夜更かしの疲れが取れて活動を開始できたのが午後を回ってから、というのを考えると、その欲望を深夜ではなく昼間に行っても結局同じだったのではないだろうか?という疑問がわいてきます。
今月の「記録しなかった長時間作業」は現在3件ありますが、いずれもその原因(太字)に「ちょっと」という言葉を使っているのが特徴的です。
「ちょっと」が爆弾のスイッチ(トリガー)になってるみたいです。
414 全体的・基本・簡潔のRPG コマンドメニュー作成(96.4%)
もし時間内に終わらない場合は:あきらめる
21:30 1:30(UNLOCK) 23:00 23:02 1:32
413 全体的・基本・簡潔のRPG コマンドメニュー作成(96.3%)
もし時間内に終わらない場合は:あきらめる
×うまくいかないなと思った
15:30 2:00(UNLOCK) 17:30 19:33 4:00 ×
412 全体的・基本・簡潔のRPG コマンドメニュー作成(96.2%)
もし時間内に終わらない場合は:あきらめる
10:50 1:30(UNLOCK) 12:20 12:22 1:31
411 × 全体的・基本・簡潔のRPG コマンドメニュー作成(96.1%)
もし時間内に終わらない場合は:あきらめる
23:00 1:30(1:30) 24:30 0:34 1:34
410 全体的・基本・簡潔のRPG コマンドメニュー作成(96%)
もし時間内に終わらない場合は:あきらめる
22:20 1:30(1:30) 23:50 0:02 1:42
409 グリフォンモデリング
もし時間内に終わらない場合は:あきらめる
22:00 1:00(2:00) 23:00 23:17 1:17
408 全体的・基本・簡潔のRPG コマンドメニュー作成(95%)
もし時間内に終わらない場合は:あきらめる
13:25 1:00(2:00) 14:25 14:35 1:10
記録しなかった長時間作業あり:
レトロゲーム「ソーサリアン」で遊んでしまった。24:00~27:00
最近新しく同じ販売店で「ザ・トリロジーズ」というレトロゲームのセットを購入(予約で発売日未定)しましたが、同じように購入した「ソーサリアン」は購入以来2年半も経っているのに、遊んだ時間は10時間程度でしかありませんでした。それで「ちょっとくらい遊ぶか…」と思って始めたら1時間程度のはずが気のゆるみで長時間(3時間)になってしまいました。
「ソーサリアン」購入目的: 学生時代に好んでいたRPGなので、RPG開発の参考に。
「ザ・トリロジーズ」購入目的: ドラクエ1の真の前身が「夢幻の心臓」(リンクはBingで「ドラクエ 夢幻の心臓」を検索)であり、「ザ・トリロジーズ」に含まれている。RPG開発の参考に。
販売店: EGGプロジェクトのAC-MALL
407 全体的・基本・簡潔のRPG コマンドメニュー作成(94.6%)
もし時間内に終わらない場合は:あきらめる
×うまくいかなくて、あきらめられない!と思った。
そういうときもある。
20:15 1:00(4:00) 21:15 22:55 2:45 ×
406 全体的・基本・簡潔のRPG コマンドメニュー作成(94.5%)
もし時間内に終わらない場合は:あきらめる
15:30 1:30(4:00) 17:00 17:01 1:31
405 全体的・基本・簡潔のRPG コマンドメニュー作成(94%)
もし時間内に終わらない場合は:あきらめる
12:35 1:30(4:00) 14:05 14:14 1:39
404 全体的・基本・簡潔のRPG コマンドメニュー作成(93%)
もし時間内に終わらない場合は:あきらめる
20:40 1:30(1:30) 22:10 22:10 1:30
403 全体的・基本・簡潔のRPG コマンドメニュー作成(92%)
もし時間内に終わらない場合は:あきらめる
21:25 1:30(1:30) 22:55 23:04 1:39
402 全体的・基本・簡潔のRPG コマンドメニュー作成(90%)
もし時間内に終わらない場合は:あきらめる
21:55 1:30(1:30) 23:25 23:35 1:40
記録しなかった長時間作業あり:
作業内容:
  1. 7/3の日記のRPGの記事で、予定していなかったプログラムリストの掲載をしよう、とちょっと思い掲載した。
  2. そのプログラムリストがだいぶ長かったので、「ダブルクリックすると全体表示/部分表示を切り替える」というしくみを作成しようと思い作成した。
  3. そして、プログラムリスト中のタブ文字による文字のズレが目にとまり、気になったので、ちゃんと整列して表示されるように直した。(コレが思っていたよりもきつかった)
タブ文字によるスペース挿入(等間隔に整列)は文章の文字数をうまく数えればできるだろうと甘く考えたが、ユニコードという文字コードは1文字3バイトになっていて、perlのlength(文字列)だとバイト数を数えるので「3文字」で計算してしまう。文章中で半角文字とユニコード文字(=漢字。横幅が半角2文字分)が混在しているとき、1文字ずつ取り出してユニコードなら2文字、そうでないなら1文字という計算をすることになってしまった。その際の「1文字ずつ取り出す」というのもそのままだと「1バイト取り出す」になってしまうので、文字コードの変換(ユニコード文字をperlの内部文字コードに変換すれば1バイトではなく1文字で取り出せる)をいちいち行う必要があった。
環境の実際の仕様と、プログラマーが勝手に思っている仕様にギャップがあると、泥沼にはまる、というのは一般的にこの先ずっと続くことなのだろうか…。
その成果のプログラムリストがコレ(リンクはスクリーンショットです。perlの正規表現を一部で使っているので意味不明に見えるところもあると思います)
  1. その記事の続きとして「どうしてそんなにRPGなのか?」という文章を書き、
  2. 動画が必要になって動画を作成し始めた。
  3. その動画のBGMについてファルコムの宣言内容を確認した
401 全体的・基本・簡潔のRPG コマンドメニュー作成(85%)
もし時間内に終わらない場合は:あきらめる
19:15 2:30(4:00) 21:45 21:42 2:27
400 全体的・基本・簡潔のRPG コマンドメニュー作成(80%)
もし時間内に終わらない場合は:あきらめる
15:35 1:30(4:00) 17:05 17:28 1:53
399 3Dお姉さんプログラム説明 3Dお姉さんプログラム説明は現在2件あるが、そのうち1件が動いていないので、それを直す。(100%)
直したページ
(このリンク先で表示される くねくね動く画面の「PROGRAM LIST」というボタンを押すと「3Dお姉さんプログラム説明」が開きます。それが今まで動作しませんでしたが今回直しました。ただし内容はあまり親切ではありません。ブラウザのキャッシュが きいていると古いファイルを読んでしまい、直したものが反映されないことがあります。動かないなと思ったらブラウザの再読み込みボタンを押してください)
もし時間内に終わらない場合は:あきらめる
21:10 0:30(1:30) 21:40 21:52 0:42
398 全体的・基本・簡潔のRPG コマンドメニュー作成(75%)
もし時間内に終わらない場合は:あきらめる
21:00 1:30(1:30) 22:30 22:37 1:37

この表の意図:

多くの人はパソコンのやりすぎやネットゲームのやりすぎには困っていると思います。

参考に言うと、この表を使う前の私は 1 回の PC 使用時間がノンストップで 17 時間というときもあったし、平均で言うと毎日 9 時間はやっていたと思います。

この表を使ってパソコンの使用時間を 事前に決めてネット上に公開 することで、パソコンのやりすぎを防止できたら、と思います。

また、数年前から考えてきましたが、そういう徹夜とか長時間作業をするよりも、昼間の短時間作業のほうが生産性は高いのではないかと思います。そういう意味でも期待しています。

※以前は NO PC WEEK と称してパソコンを使用しない期間を設けることでやりすぎに対処してきましたが、もっと具合の良い方法はないかと考え、この表を使うようになりました。

記入の法則:

  • 日付は表示していません(私の生活パターンをすべて知らせるのはよくないから)。しかし、白と灰色の色分けは、同じ色の連続で同じ日を表しています。
  • 左端の「A1. 運動」、「A2. 1問」、「A3. 運動」について
    この表の目的とは異なりますが、ついでとして、遊び100%の毎日を送るときでも勉強の習慣を忘れないために、たった1問で良いので解くことにします。
    正直言うと毎回遊ぶ前に必ず1問勉強するのは心が折れそうです。でも慣れさえすれば…と思います。
    行ったら◎、行わなかったら× を記入します。
    2020年11月20日ぐらいから「A1. 運動」を追加しました。パソコンを行う前に運動することを強制するものです。腕立て伏せ10回とか腹筋10回とかです。
    (ホントだったら30回くらいはやりたいところですが、私は体の調子が悪いので、10回程度にしています)
    2021年6月26日「A3. 運動」を追加。PC使用の終了時にも運動する。
    A1はPC漬けによる筋力衰え対策の意味で、筋トレを行い、
    A3はPC作業でこった体をほぐす意味で、ラジオ体操やストレッチ体操を行う。
  • 右端の「G. 判定」について
    「D. 予定終了時刻」と「E. 実際終了時刻」を比べて、オーバーした時間によって判定を行います。
    ◎ 9分以下
    ○ 10分~19分
    △ 20分~29分
    × 30分以上 (オーバー理由を記載する。理由の統計を取れば何が問題なのか把握でき、改善しやすいです)
  • 平日の 1.5h、2.0h の PC 使用の連続が負担になっているので、週のスケジュールは下記のようにする。
    月水で PC 使用しないことでうまいぐあいに「体力回復」されて、「生活」がうまく回り、プログラミング以外の「創作活動」(イラスト等)に時間が取れることを期待します。
    月:0h
    火:1.5h
    水:0h
    木:1.5h
    金:1.5h
    土:4hまで(休日で、翌日も休日)
    日:2hまで(休日で、翌日平日) ←早起きなおかつ(日々の買い物とかじゃなく)散歩するなら 3h やって良い
    (※2021年2月2日:週の各時間を調整しました)
  • ×、△、"記録しなかった長時間作業" が多いと思ったら、バランスをとるために、NO PC WEEK※ を1週間実施する。
    NO PC WEEK とは私自身の健康のために私のパソコンの使用を制限する期間です)


ちなみに、分単位で記録を取ったりして、だいぶマメに見えるかもしれませんが、Windows の日本語入力(MS-IME)で「いま」と入力し、 変換 キーを押さずに ボタンを押すと現在の時刻になります。道具の便利さが人をマメに見せるのかもしれません。

例外事項:

  • 「E. 実際終了時刻」のあと、プログラミングの場合のみスッパリ終了しないで、今後のプログラミングの方針をテキストファイルに書くのは OK にしています。


「スーパーPC WEEK」:

連休中(3連休以上)に、NO PC WEEK をオフにして好きなだけパソコンを使ってよいとする期間を、「NO PC WEEK」に対して「スーパーPC WEEK」と言う。

ただし以下の決まりを守ること。

  • その日に自由にパソコンを使ってもよいが、1回あたり、いつものように表に記入すること。(開始時運動、勉強1問、終了時運動も行うこと)
     (理由: 表が結構有効だから)
  • 1回ごとに、掃除、炊事、洗濯や、別の趣味など、まとまった作業を はさむこと。
     (理由: 1回1回がが連続してつながると回を分けている意味(身体に無理をかけない)がなくなるから)
  • 24時に就寝するよう努めること。(強制ではないが、つとめること)
     (理由: 深夜の神秘的な時間は魅力だが、安定したリズムも好ましいから)
  • 連休の最終日は朝から「NO PC WEEK」再開とする。
     (理由: 翌日は仕事なので影響するから)

なお、表の中央やや右寄りの「C. 予定 使用時間(当日限度)」列の "当日限度" には UNLOCK と記入する。


中途結果:

結構いい結果になっています。炊事や掃除、散歩、早起きなどが好ましいリズムでできるようになりました。

(2020年9月6日追記:散歩、早起きは最近あまりできていません。掃除や炊事は理想的にできています)

また、パソコン以外の趣味も進むようになりました。電子回路、ガンプラ、RPG のプログラム以外のモンスターイラストやストーリーなど RPG の肉付け部分の創作、勉強、昔好きだったペーパークラフト等々。

そしてパソコンの趣味自体も深夜遅くまで行うよりも質が高くなったように感じます。制限された短い時間の中で結果を得ようとするので、取捨選択が行われているし、時間が終了して、空いた時間ができ、それがほどよい休憩になり、今後の作業の方針を落ち着いて検討することもできます。それが質につながっているのかなと思います。

この取り組みが、15 才 ~ 18 才くらいまでの高専(中退)に所属していた時に実施できていたら良かっただろうなと思います。でもそれくらいの年齢では経験が浅く、このような効果的なルール作りを行うことはできなかったと思います。自分は人に比べて「創作意欲」や「ゲームで遊ぶ欲望」におぼれやすいところがあり、そのコントロールはとても難しいです。


自分の家族にすすめたい方へ:


(これは イラストAC の無料素材です)

パソコンのやりすぎやネットゲームのやりすぎは社会問題にもなっているので、「うちの子についてなんとかしないと…」と思っているご家族の方は多くいらっしゃると思います。

私の両親も過去に私について問題にしていました。学校へ行かず、毎日朝までパソコンに向かい、悶々としていたんです。


この表はその家族が困っていたときから 30 年後に、私が自分で必要を感じて作ったものです。

私は今 一人暮らしをしていて、自分で生計を立てる中、パソコンにおぼれた生活をすると、生活がうまく回らなくなるんです。

具体的には、

  • 人前で疲れた顔を見せてしまい、人間関係がうまくいかなくなる。もっと具体的には、職場、とこや、お店のレジ、歯医者。
  • 掃除、洗濯、炊事が後回しになり、実質、それらを行う時間がなくなってしまう。深夜遅くや翌日に回したり、行わなかったりする。

これらを改善するために表を作りました。


でも、このような必要にせまられて「自分の動機で始めた場合」と、「人からすすめられて始めた場合」とでは、結果が異なると思います。

自分の切実な動機で始めたなら自分から進んでこの表を活用すると思いますが、外から押し付けられたものはなかなか定着しないものです。

あまり適当なことは言えませんが、「中途結果」タブの中の青い部分で書いたことは、本人にとって得になることなので、「ときどき休憩して、他のあの趣味やってみたらどうだ?」とか「ときどき休憩したほうがプログラミングの質が上がるって話だぞ?」という形ですすめてみたらどうでしょうか。(それでも最終的には自立してもらうことは必要だと思いますが)


私が両親を困らせていたときに、突然、外へ一人で出て行って、一人暮らしを始めたり、接客業を始めたり、いくつか資格取得したりといろいろ行えた理由というのは、正直言ってわかりません。(※しかし途中で失業して2度、実家に戻ったことがあります。1 回目は 21 才くらいのときに 5 年間、2 回目は 35 才くらいのときに1年未満、実家にいて、何もしてなかったり働いたりしていました)

私が両親を困らせていたのは 16 才 ~ 20 才くらいの学生のころですが、そのころ家族と私自身と友人たちがみんなそれぞれ、私の生活について心配したり困ったり悩んだり、あの手この手を試したりしていました。そういう煮詰まったような状況が運命をそのように(解決の方向へ)動かすのかもしれません。運命がどうの というのは変ですが、そのくらいのことしか言えません。何かしら取り組む必要があるということですかね。


この社会問題はクリアーすべきものみたいです。



2021/7/25(日)

全体的・基本・簡潔のRPG -[RPG,javascript]

試作の動作を試す

最近私が趣味で作っている RPG の試作品の、動くものを掲載します。

ただし、キーボードが必要で、タッチデバイスでは動かせません。


この画像リンクをクリックすると新しいウィンドウを開いて JavaScript を実行します。

※Google Chrome で描画がおかしくなっていたようですが、修正しました。ただしブラウザの「キャッシュ」が機能していると修正前のプログラムのままになってしまいます。キャッシュを読まないでそのページを読み取る方法は…


Windows Mac
Google Chrome SHIFT + F5 Command + Shift + R
Fire fox CTRL + F5 Command + Shift + R


そのプログラムリストについて

そのプログラムリストです。全行数は 1838 行。

この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。


/* memo.

名前のすみわけ(理想であって、実際はそうなっていないかもしれない)

"イベント"

イベントドリブンのイベント イベント

シナリオスクリプトのイベント ストーリー

"アイテム"

もちもの トレジャー

メニュー項目 アイテム

座標

グラフィクス gx, gy, gw, gh

マス目のある画面 col, row, cols, rows

データ上の位置 x, y, w, h


配列変数の名前と、同じデータを持つ連想配列変数の名前

配列 -s たとえばcharacters

連想配列 -z たとえばcharacterz


そのとき、たとえばsprites、spritezの読み方について

配列時 sprites スプライトス

連想配列時 spritez スプライトス

※どちらも-スと読む。つまり-zは見た目の区別でしかない

連想配列のkeyとキー入力のkey

連想配列 name

キー入力 key

言葉の意味

tweak 調整する

*/


// |

// V

class Utl {//c

static arrayForward( array, element ) {//m

let idx = array.indexOf( element );

//check.

if( idx == -1 ) {

alert( "Utl.arrayForward() 失敗した" );

return;

}


array.splice( idx, 1 );

array.push( element );

}

static accessBy( object /*keys*/ ) {//m

for( let i = 1; i < arguments.length; i++ ) {

object = object[ arguments[ i ] ];

if( object == null ) return null;

if( typeof object === "undefined" ) return undefined;

}

return object;

}

static array2object( array, object, keyMaker ) {//m

for( let element of array ) {

object[ keyMaker( element ) ] = element;

}

}


static objectFrom( array, keyMaker ) {//m

let object = new Object();

for( let element of array ) {

object[ keyMaker( element ) ] = element;

}

return object;

}

static lengthBin( str ) {//m

//漢字を2バイト、半角を1バイトとして計算 (UTF8を前提)

return encodeURI( str ).replace( /%20/, "*" ).replace( /%..%..%../g, "**" ).length;

}

static lengthBinDiv2( str ) {//m

//lengthBの結果を2で割り、小数点切り上げ

return Math.ceil( Utl.lengthBin( str ) / 2 );

}

static maxLength( strArray ) {//m

let v = 0;

for( let str of strArray ) v = Math.max( v, String( str ).length );

return v;

}

static maxLengthBin( strArray ) {//m

let v = 0;

for( let str of strArray ) {

v = Math.max( v, Utl.lengthBin( String( str ) ) );

}

return v;

}

static maxLengthBinDiv2( strArray ) {//m

return Math.ceil( Utl.maxLengthBin( strArray ) / 2 );

}

static objectCopy( object ) {

let copy = new Object();

for( let key in object ) {

copy[ key ] = object[ key ];

}

return copy;

}

}//Utl


class Element {//c

constructor( style, app ) {

this.style = style;

this.app = app ? app : this;


//check.

if( this.style.cellSize === undefined ) {

this.style.cellSize = this.app.style.cellSize;

}


this.name = this.constructor.name;


this.x = this.style.col * this.style.cellSize;

this.y = this.style.row * this.style.cellSize;

this.w = this.style.cols * this.style.cellSize;

this.h = this.style.rows * this.style.cellSize;


if( this.style.cellSize == undefined )

this.style.cellSize = 5; //異常をわかりやすくするために5

if( this.style.fontSize == undefined )

this.style.fontSize = this.style.cellSize;


this.childs = new Array();

this.floats = new Array();

}

typeKey( key ) {//m

}

senseKey( key ) {//m

}

draw( cc ) {//m

this.drawWindow( cc );

this.drawChildren( cc );

}

drawWindow( cc ) {

//ウィンドウ

let x = this.x - this.style.cellSize / 2;

let y = this.y - this.style.cellSize / 2;

let w = this.w + this.style.cellSize;

let h = this.h + this.style.cellSize;


//ウィンドウ 面

cc.fillStyle = "white";

cc.fillRect( x, y, w, h );


//ウィンドウ 枠

if( this.app.currentElement == this )

cc.strokeStyle = "yellow";

else

cc.strokeStyle = "black";

cc.strokeRect( x, y, w, h );

}

drawChildren( cc ) {

for( let child of this.childs ) child.draw( cc );

for( let float of this.floats ) float.draw( cc );

}

searchStory() {//m

}

focus() {

/*

下記記述でもよいが、focus()と書いたほうがわかりやすいと思って。

*/

this.app.currentElement = this;

}

appendChild( child ) {//m

if( child.style.isFloat ) {

this.floats.push( child );

} else {

this.childs.push( child );

child.focus();

}

child.parent = this;

return child;

}

removeChild( child ) {//m

let idx;

console.log( "remove", child.name, "from", this.name );

//floatの場合、floatsから削除

if( ( idx = this.floats.indexOf( child ) ) > -1 ) {

this.floats.splice( idx, 1 );

} else if( ( idx = this.childs.indexOf( child ) ) > -1 ) {

//childの場合、childsから削除

this.childs.splice( idx, 1 );

}


//メッセージウィンドウの場合はthis.appからも削除

if( child == this.app.messageWindow ) {

delete this.app.messageWindow;

}


//削除したものがカレントだった場合、バトンタッチ

if( child == this.app.currentElement ) {

let nextCurrent = this.searchNextCurrent();

nextCurrent.focus();

console.log( "next", nextCurrent.name );

}


return child;

}

// |

// V

searchNextCurrent() {//m

/*

親子関係において、子が閉じられたとき、フォーカスをどの要素に移すか。

removeChildから呼ばれる。

*/

if( this.childs.length ) {

return this.childs[ this.childs.length - 1 ];

} else {

return this;

}

}


}//Element

// |

// V

class App extends Element {//c

constructor( canvasId ) {

let canvas = document.getElementById( canvasId );


canvas.width = 256;

canvas.height = 224;

let cellSize = 16;


super( {

col : 0,

row : 0,

cols : Math.floor( canvas.width / cellSize ),

rows : Math.floor( canvas.height / cellSize ),

cellSize : cellSize,

}, null );

this.cc = canvas.getContext( "2d", { alpha : false } );


//---メトロノーム

this.metronomez = new Object();

this.addMetronome( "t1000_i2" );

this.addMetronome( "t500_i2" );

this.addMetronome( "t250_i2" );

//カーソル専用(手動追加)

this.metronomez.forCursor = {

maxTime : 500,

maxIndex : 0,

time : 0,

index : 0,

toggle : true,

changed : false,

}

/*

メトロノームの各フラグの変化タイミング図解(タイミングチャート)

maxTime : 250,

maxIndex : 3, の場合の各フラグの動き↓


(経過時間) 0 ... 250 ... 500 ... 750 ... 1000 ...(ms)


time : 0, 0 ..249 0 ..249 0 ..249 0 ..249 0 .. ...(ms)


index : 0, 000000001111111122222222000000001111 ...


toggle : true, ~~~~~~~~________~~~~~~~~________~~~~ ...


changed : false, ________~_______~_______~_______~___ ...


~ : true

_ : false

... : 略

※このグラフ中の1文字単位の縦1列はrequestAnimationFrameに相当する


つまり、requestAnimationFrameごとに、

timeは経過時間を増やしていきmaxTimeになると0に戻る

indexはtimeがmaxTimeになるごとに1増え、maxIndexになると0に戻る

toggleはtimeがmaxTimeになるごとに真偽を反転する

changedはtimeがmaxTimeになったときだけ真になる


maxTimeでアニメのパラパラのスピード決め

toggleやchangedでアニメのパラパラの実行を判断

indexでアニメのパラパラの画の切り替え

*/


this.spritez = {

player : {

images : [ "←", "-", "↑", "|", "→", "-", "↓", "|" ],

metronome : this.metronomez.t500_i2,

},

}


this.anms = new Array();


this.artz = {

spritez : this.spritez,

bgms : new Object(),

soundz : new Object(),

}


this.screens = new Array();

this.screenz = new Object();


this.characters = new Array();

this.characterz = new Object();

this.treasureCatalog = new Object();


//---メニュー

this.menusrcs = [

{

name : "yesno",

title : "",

items : [

{

name : "はい",

},

{

name : "いいえ",

}

],

},

{

name : "camp",

title : "コマンド?",

argzkey : "cmd",

items : [

{

name : "どうぐ",

title : "だれの??",

items : this.characters,

argzkey : "char1",

itemsMenu : {

title : "どれ?",

argzkey : "dougu",

row : 3, //だれでも表示高さを固定して表示

},

},

{

name : "しらべる",

},

{

name : "まほう",

title : "だれの?",

items : this.characters,

argzkey : "char1",

},

],

},

//---

{

name : "つかう",

title : "だれに?",

items : this.characters,

argzkey : "char2",

script : async function( argz ) {

await this.tell( argz.char1.name + "は" + argz.char2.name + "に" + argz.dougu.name + "を使った!", { wait : true, close : true } );


let lostFlg = true;

if( argz.dougu.limit != undefined ) {

//limitが0になったらなくなるタイプ

argz.dougu.limit --;

//check.

lostFlg = argz.dougu.limit == 0;

if( lostFlg ) {

await this.tell( "なんと " + argz.dougu.name + " は壊れてしまった!", { wait : true, close : true } );

}

}

if( lostFlg ) {

let idx = argz.char1[ argz.cmd.name ].indexOf( argz.dougu );

argz.char1[ argz.cmd.name ].splice( idx, 1 );

}

},

},

{

name : "わたす",

title : "だれに?",

items : this.characters,

argzkey : "char2",

script : async function( cmd, char1, dougu, cmd2, char2 ) {

await this.tell( char1.name + "は" + char2.name + "に" + dougu.name + "を渡した!", { wait : true, close : true } );

},

},

{

name : "すてる",

script : async function( cmd, char1, dougu, cmd2 ) {

await this.tell( char1.name + "は" + dougu.name + "を捨てた!", { wait : true, close : true } );


},

},

]

this.menusrcz = Utl.objectFrom( this.menusrcs, element => element.name );


//---トレジャー

this.treasureCatalog = {

"どうけん" : {


},

"せいどうけん" : {


},

"てっけん" : {


},

"こうてつけん" : {


},

"ひほう" : {

limit : 3,

contextMenu_camp : {

title : "どうする?",

items : [

this.menusrcz[ "つかう" ],

this.menusrcz[ "わたす" ],

this.menusrcz[ "すてる" ],

],//items

},

},

"なんこう" : {

contextMenu_camp : {

title : "どうする?",

argzkey : "dousuru",

items : [

this.menusrcz[ "つかう" ],

this.menusrcz[ "わたす" ],

this.menusrcz[ "すてる" ],

],//items

},//nextMenu

contextMenu_battle : this.menusrcz[ "つかう" ],

},//なんこう

}//treasureCatalog


//tweak. キーを名前として要素内に登録

for( let key in this.treasureCatalog ) {

this.treasureCatalog[ key ].name = key;

}


//---キャラクター


this.characters.push( {

name : "キャラ1",

"どうぐ" : [

{ name : "つるぎ" },

this.treasureCatalog[ "なんこう" ],

Utl.objectCopy( this.treasureCatalog[ "ひほう" ] ),

{ name : "つるぎ" },

],

} );


this.characters.push( {

name : "キャラ2",

"どうぐ" : [

{ name : "なんこう2" },

{ name : "つるぎ" },

],

} );

this.characters.push( {

name : "キャラ3",

"どうぐ" : [

{ name : "なんこう3" },

{ name : "つるぎ" },

],

} );

Utl.array2object( this.characters, this.characterz, element => element.name );



let dir = location.pathname.replace( /%20/g, " " ).replace( /\/[a-z0-9_ .-]*$/i, "" ) + "/";

//素材のプリロード

if( 1 ) {

return Promise.all( [

this.load( "bgm", "field", dir + "_bgm/街/MusMus-CT-NV-24/MusMus-CT-NV-24.mp3" ),

this.load( "bgm", "town", dir + "_bgm/街/MusMus-BGM-078/MusMus-BGM-078.mp3" ),

// this.load( "bgm", "battle", dir + "_bgm/battle.wav" ),

this.load( "sound", "pushA", dir + "_sound/pushA.mp3" ),

this.load( "sound", "pushWall", dir + "_sound/pushWall.mp3" ),

this.load( "sound", "walk", dir + "_sound/walk.wav" ),

] ).then( function() {

return this; //constructorだからインスタンスを返すこと

}.bind( this ) );

} else {

return Promise.all( [

this.load( "bgm", "field", dir + "_bgm/10 イルバーンズの遺跡 (遺跡・地上).wav" ),

this.load( "bgm", "town", dir + "_bgm/1-05 街.wav" ),

this.load( "bgm", "battle", dir + "_bgm/battle.wav" ),

this.load( "sound", "pushA", dir + "_sound/pushA.mp3" ),

this.load( "sound", "pushWall", dir + "_sound/pushWall.mp3" ),

this.load( "sound", "walk", dir + "_sound/walk.wav" ),

] ).then( function() {

return this; //constructorだからインスタンスを返すこと

}.bind( this ) );

}

}//constructor()

// |

// V

//プリロード

load( type, name, src ) {//m

return new Promise( function( endmark ) {

let object;

console.log( type, name, "loading.." );

switch( type ) {

case "bgm":

object = new Audio();

object.oncanplaythrough = function() {

this.artz.bgms[ name ] = object;

object.oncanplaythrough = null;

console.log( "\t", type, name, "loaded." );

endmark();

}.bind( this );

object.src = src;

object.load();

break;

case "sound":

object = new Audio();

object.oncanplaythrough = function() {

this.artz.soundz[ name ] = object;

object.oncanplaythrough = null;

console.log( "\t", type, name, "loaded." );

object.rapidPlay = function() {

object.pause();

object.currentTime = 0;

object.play();

/*

同じ音楽ファイルの連続演奏時は

最初にリセットをかける必要がある。

rapid=急速

*/

}

endmark();

}.bind( this );

object.src = src;

object.load();

break;

}

}.bind( this ) );

}//load()

// |

// v

stop() {//m

cancelAnimationFrame( this.timerId );

}

start() {//m

//------------


this.focus();


this.keys = new Array();

onkeydown = function( e ) {


if( this.keys.indexOf( e.which ) == -1 ) {

this.keys.push( e.which ); //キーセンス オン


this.currentElement.typeKey( e.which );

//キータイプ 実行

}

}.bind( this );

onkeyup = function( e ) {

let idx = this.keys.indexOf( e.which );

if( idx > -1 ) this.keys[ idx ] *= -1;

//キーセンス オフ要求(同値のマイナスで示す)


}.bind( this );


this.status = {

player : {

position : {

map : App.worldmap,

x : 0,

y : 0,

},

direction : 0,

gold : 200,

},

}


//タイトル画面から始める

let titleScreen = new TitleScreen( {

col: 0,

row : 0,

cols : this.style.cols,

rows : this.style.rows,

cellSize : 8,

}, this );

this.appendChild( titleScreen );



this.beforeTime = 0;

this.frame( 0 );


}//constructor()

// |

// V

frame( tm ) {//m

//経過時間

let diff = tm - this.beforeTime;

this.beforeTime = tm;


//メトロノーム更新

for( let key in this.metronomez ) {

let metronome = this.metronomez[ key ];

metronome.time += diff


//check. そのメトロノームは時を刻んだ

if( metronome.time >= metronome.maxTime ) {

metronome.time = 0;

metronome.toggle = ! metronome.toggle;

metronome.changed = true;

metronome.index ++;

//check. インデックスリセット

if( metronome.index == metronome.maxIndex ) metronome.index = 0;

} else {

metronome.changed = false;

}

}


//キーセンス 処理

for( let i = this.keys.length - 1; i >= 0; i-- ) {

let key = this.keys[ i ];

this.currentElement.senseKey( Math.abs( key ) );

//check. キーセンス オフ

if( key < 0 ) this.keys.splice( i, 1 );

}


//1ドットスクロール実行

if( this.mapScreen )

if( this.mapScreen.scroll.isAnimating ) {

this.mapScreen.frameForScroll();

}


//ストーリー検索&実行

let story;

if( story = this.currentElement.searchStory() ) {

story.begin.call( this );

}


//アニメ処理 (forの途中で要素削除するので逆順)

for( let i = this.anms.length - 1; i >= 0; i-- ) {

if( this.anms[ i ]() ) this.anms.splice( i, 1 );

}


this.draw( this.cc );


this.timerId = requestAnimationFrame( this.frame.bind( this ) );

}

// |

// V

typeKey( key ) {//m

//check.

if( key == 80 ) {

if( this.timerId ) {

cancelAnimationFrame( this.timerId );

this.timerId = null;

this.cc.fillStyle = "yellow";

this.cc.fillText( "PAUSE", 100, 100 );

} else {

this.frame();

}

}


}

async tell( message, flgs ) {//m

//check. メッセージウィンドウがないときは作成する

if( ! this.messageWindow ) {

this.messageWindow = new MessageWindow( this.app );

this.currentElement.appendChild( this.messageWindow );

}

this.messageWindow.focus();

await this.messageWindow.write( ...arguments );

}

// |

// V

yesno( closeFlg ) {//m

this.artz.soundz.pushA.rapidPlay();


let yesnoMenu = new Menu( "selectmode", "", this.menusrcz.yesno, {

col :27,

row : 24,

}, this );

this.mapScreen.appendChild( yesnoMenu );


return new Promise( function( endmark ) {

yesnoMenu.endmark = endmark;

}.bind( this ) ).then( function( selectedItem ) {

this.mapScreen.removeChild( yesnoMenu );

if( selectedItem )

return selectedItem.name == "はい";

else

return false;

}.bind( this ) );

}

// |

// V

async shop() {//m

let treasureMenu = new Menu( "selectmode", "", {

name : "shop",

title : "刀剣屋 'ロバートウッドテイル'",//全13, 半3

items : [

{ name : this.treasureCatalog[ "どうけん" ].name, price : 100, },

{ name : this.treasureCatalog[ "せいどうけん" ].name, price : 200, },

{ name : this.treasureCatalog[ "てっけん" ].name, price : 300, },

{ name : this.treasureCatalog[ "こうてつけん" ].name, price : 400, },

],

columns : [

{

key : "name",

},

{

key : "price",

align : "right",

}

],

}, {

col : 2,

row : 2,

}, this );

this.mapScreen.appendChild( treasureMenu );


//所持金表示

let stat = new List( {

columns : [ { key : "title" }, { key : "value", align : "right" } ],

items : [

{

title : "エン",

value : function() { return this.status.player.gold; },

},

],


}, {

col : treasureMenu.style.col + treasureMenu.style.cols + 2,

row : treasureMenu.style.row,

isFloat : true,

}, this.app );

treasureMenu.appendChild( stat );


while( 1 ) {

await this.tell( "何を買うんだ?\n" );


//商品を選ぶ

let selectedItem = await new Promise( function( endmark ) {

treasureMenu.endmark = endmark;

treasureMenu.focus();

}.bind( this ) );


//check. お店から退出

if( ! selectedItem ) break;


await this.tell( selectedItem.name + "だな。\n" );

await this.tell( selectedItem.price + "エンだ。買うかね?\n" );

if( await this.yesno() ) {

this.status.player.gold -= selectedItem.price;

await this.tell( "まいどあり!\n", { wait : true } );

await this.tell( "\n" );

}

}

await this.tell( "ありがとうございました!", { wait : true, close:true } );

await this.mapScreen.removeChild( treasureMenu );

}


addMetronome( name /*other args*/ ) {//m

//オーバーロード(メソッド名が同じで引数のパターンが異なる)の模造

//”関数の先頭で引数の型を判定する条件分岐で対応”

let argsId = "";

for( let arg of Array.from( arguments ) ) {

argsId += "," + ( typeof arg ).substr( 0, 1 );

}

let metronome;

switch( argsId ) {

case ",s" : //フォーマット名称 t時間_iインデックス上限

let tokens = name.split( /_/ );

metronome = {

maxTime : Number( tokens[ 0 ].substr( 1 ) ),

maxIndex : Number( tokens[ 1 ].substr( 1 ) ),

time : 0,

index : 0,

toggle : false,

changed : false,

}

this.metronomez[ name ] = metronome;

break;

case ",s,n" : //自由な名称、時間

let tm = arguments[ 1 ];

metronome = {

maxTime : tm,

maxIndex : 0,

time : 0,

index : 0,

toggle : false,

changed : false,

}

this.metronomez[ name ] = metronome;

break;

default:

alert( "addMetronome()にて、未定義の引数リスト:[" + args + "]" );

}

return metronome;


}//addMetronome()

}//App



class MessageWindow extends Element {//c

constructor( app ) {

super( {

col : 1,

row : 12,

cols : 19,

rows : 5,

cellSize : 12,

}, app );


this.matrix = new Array();

for( let r = 0; r < this.style.rows; r++ ) {

this.matrix.push( new Array() );

}

this.matrixCursorCol = 0;

this.matrixCursorRow = 0;


//フラグ

this.endedByOutside = false;

}

write( message, flgs ) {//m

//check.

if( ! flgs ) {

flgs = {

wait : false, //メッセージ表示後キー入力待ちしたいとき

close : false, //メッセージ表示後ウィンドウを閉じたいとき

}

}

//check. "外部により終了させられた"フラグ(1文字ずつ表示を一気に等)

this.endedByOutside = false;


this.message = message;

this.flgs = flgs;


return new Promise( function( endmark ) {

this.endmark = endmark;

this.metronome = this.app.addMetronome( "test1", 100 );

this.seek = 0;

//1文字ずつ表示するアニメーション

this.app.anms.push( function() {


//check. メトロノームに合わせる

if( ! this.metronome.changed ) return;


//終了チェック兼1文字出力

if( this.endedByOutside || ! this.writeChar() ) {

//check. キー待ちしない

if( this.flgs.wait == false ) this.endmark();

//キー待ちする場合は、typeKey()のほうでthis.endmark()される


return true; //アニメ終了の意

}

}.bind( this ) );//anm

}.bind( this ) ).then( function() {

//endmark()されたらここに来る

//MessageWindowのclose指定

if( this.flgs.close ) {

this.parent.removeChild( this );

}

}.bind( this ) );//new Promise

}//write()

// |

// V

writeChar() {//m

/* memo.

messageの末尾時にfalseを返す

*/

let ch = this.message.substr( this.seek, 1 );

let kaigyo = false;


switch( ch ) {

case "\n":

kaigyo = true;

break;

default:

this.matrix[ this.matrixCursorRow ][ this.matrixCursorCol ] = ch;

this.matrixCursorCol ++;

//check. 右端

if( this.matrixCursorCol == this.style.cols ) kaigyo = true;

}//switch


//check. 改行

if( kaigyo ) {

this.matrixCursorCol = 0;

//ウィンドウの途中行

if( this.matrixCursorRow < this.style.rows - 1 ) {

this.matrixCursorRow ++;

} else {

//ウィンドウの最下行

//1行スクロール

this.matrix.shift();

this.matrix.push( new Array() );

}

}


this.seek ++;


return this.seek < this.message.length; //messageの末尾時false


}//writeChar()

typeKey( key ) {

if( this.seek < this.message.length ) {

//文字出力中のときは文字出力を一気に行う

for( let i = this.seek; i < this.message.length; i++ ) {

this.writeChar();

}

this.endedByOutside = true;

} else if( this.flgs.wait ) {

//文字出力終了のときは、呼び出し元に処理終了を知らせる。

this.endmark();

}

}//typeKey()

draw( cc ) {

super.draw( cc );

cc.save();

cc.translate( this.x, this.y );


cc.font = this.style.fontSize + "px ''";

cc.fillStyle = "black";

for( let r = 0; r < this.style.rows; r++ ) {

let gy = this.style.fontSize * r;

for( let c = 0; c < this.style.cols; c++ ) {

let gx = this.style.fontSize * c;

let ch = this.matrix[ r ][ c ];

//check.

if( ! ch ) continue;

cc.fillText( ch, gx, gy + this.style.fontSize );

}

}


cc.restore();

}//draw()

}//MessageWindow

// |

// V

//---●List

class List extends Element {//c

constructor( src, style, app ) {

//check.

if( style.cellSize == undefined ) style.cellSize = 8;

//check. 行数と列数を計算

if( style.cols == undefined ) {

style.cols = 0;

//各列において、その全行の文字列の中での最大文字列数を得る

//その最大文字列数をstyle.colsへ加算していき、ウィンドウの横幅とする

for( let column of src.columns ) {

let strings = new Array();

for( let item of src.items ) {

let string = item[ column.key ] == undefined ? item.ref[ column.key ] : item[ column.key ];

if( string instanceof Function ) string = string.call( app );

strings.push( string );

}

column.cols = Utl.maxLengthBinDiv2( strings ) + 1; //その列の幅

column.col = style.cols; //その列の位置

style.cols += column.cols;

}

//check. タイトルのほうが長ければ

if( src.title != undefined ) {

style.cols = Math.max( Utl.lengthBinDiv2( src.title ), style.cols );

}

//check.

if( style.paddingLeft != undefined ) style.cols += style.paddingLeft;

}//if

if( style.rows == undefined ) {

style.rows = src.items.length;

//check.

if( src.title ) style.rows++;

}

super( style, app );

this.src = src;


}

draw( cc ) {//m

this.drawWindow( cc );


//debug. テキストエリアに水色枠

if( 0 ) {

cc.strokeStyle = "cyan";

cc.strokeRect( this.x, this.y, this.w, this.h );

}

let gx, gy, gw, gh;

cc.save();


//原点を描画位置へ移動

gx = this.style.col * this.style.cellSize;

gy = this.style.row * this.style.cellSize;

cc.translate( gx, gy );


cc.fillStyle = "black";

cc.font = this.style.fontSize + "px ''";


//タイトル

if( this.src.title ) {

gx = ( this.style.paddingLeft - 0.3 ) * this.style.fontSize;

gy = 0.65 * this.style.fontSize;

cc.fillText( this.src.title, gx, gy );

}


this.translateToItemArea( cc );


for( let i = 0; i < this.src.items.length; i++ ) {

let item = this.src.items[ i ];

let gy = this.style.cellSize * i;

for( let col = 0; col < this.src.columns.length; col++ ) {

let column = this.src.columns[ col ];

let str = item[ column.key ];

//check.

if( str instanceof Function ) str = str.call( this.app );

let gx = column.col * this.style.cellSize;

if( Utl.accessBy( column, "align" ) == "right" ) {

gx += column.cols * this.style.cellSize;

gx -= cc.measureText( str ).width;

}


//check.

if( typeof str === "undefined" ) {

str = item.ref[ column.key ];

}

cc.fillText( str, gx, gy + this.style.cellSize );

//debug. 列に緑枠

if( 0 ) {

cc.strokeStyle = "green";

cc.strokeRect(

column.col * this.style.cellSize,

gy,

column.cols * this.style.cellSize,

this.style.cellSize

);

}

}//for

}//for

cc.restore();

}//draw()

translateToItemArea( cc ) {//m

if( this.src.title ) cc.translate( 0, this.style.fontSize );

cc.translate( this.style.paddingLeft * this.style.cellSize, 0 );

}

}//List

// |

// V

//---●Menu

class Menu extends List {//c

constructor( mode, context, src, style, app ) {

//check.

if( src.columns == undefined ) {

src.columns = [

{

key : "name",

},

]

}

//check.

if( style.paddingLeft == undefined ) style.paddingLeft = 1;

super( src, style, app );


this.mode = mode;

this.context = context;

this.src = src;

//check.

if( this.src.context ) this.context = this.src.context;


this.itemCols = 1;

this.itemRows = this.src.items.length;

this.cursorX = 0;

this.cursorY = 0;

}

typeKey( key ) {//m

let addX = ( key == 39 ) - ( key == 37 );

let addY = ( key == 40 ) - ( key == 38 );


if( addX || addY ) {

this.cursorX += addX;

this.cursorY += addY;

//check.

if( this.cursorY < 0 ) this.cursorY = 0;

if( this.cursorY >= this.itemRows ) this.cursorY = this.itemRows - 1;

//カーソル 動いた時点で「点滅の点灯開始状態」にする

//こうしないとチラチラして見づらい

this.app.metronomez.forCursor.toggle = true;

this.app.metronomez.forCursor.time = 0;

} else {


switch( key ) {

case 32://SPACE

this.app.artz.soundz.pushA.rapidPlay();


this.selectedItem = this.src.items[ this.cursorY ];


//選択した項目の形態

let menusrc;

//選択した項目はメニューであるA

if( this.selectedItem.items ) {

menusrc = this.selectedItem;

} else if( this.src.itemsMenu ) {

//選択した項目はメニューではないが、

//this.srcにitemsMenu指定がある。B

menusrc = Utl.objectCopy( this.src.itemsMenu );

menusrc.items = this.selectedItem[ this.src.name ];

} else if( this.selectedItem[ "contextMenu_" + this.context ] ) {

//選択した項目はメニューではないが、

//this.selectedItem内にcontextMenu指定がある。C

menusrc = this.selectedItem[ "contextMenu_" + this.context ];

} else {

//メニューの終了

if( this.mode == "selectmode" )

this.endmark( this.selectedItem);

else if( this.mode == "executemode" ) {

this.execute();

} else {

alert( [ "undefined mode:", this.mode, "at menu.typeKey()" ].join( " " ) );

}

return;

}


//次のメニューを表示

let col;

if( menusrc.col == undefined )

col = this.style.col + this.selectedItem.name.length + 2

else

col = menusrc.col;

let row;

if( menusrc.row == undefined )

row = this.style.row + this.cursorY + 2;

else

row = menusrc.row;


let menu = new Menu( this.mode, this.context, menusrc, {

col : col,

row : row,

}, this.app );

this.appendChild( menu );


break;

case 66://B

if( this.endmark ) this.endmark( null );//ショップ等 ストーリー時

else this.parent.removeChild( this ); //キャンプ等 非ストーリー時

break;

default:

console.log( key );

}//switch

}//if else

}//typeKey()

// |

// V

async execute() {//m

let menus = new Array();

let element = this;

while( element instanceof Menu ) {

menus.unshift( element );

element = element.parent;

}


let script;

let argz = new Object();


for( let menu of menus ) {

if( menu.src.script ) script = menu.src.script;

argz[ menu.src.argzkey ] = menu.selectedItem;

console.log( menu.src.title, menu.src.argzkey );

}

//check.

if( ! script ) {

alert( "scriptが未定義の状態です。at menu.execute()" );

}


await script.call( this.app, argz );


//check. scriptによってサブメニューの元となったモノがなくなった。

//たとえば、道具:薬草>どうする:使う>だれに:キャラ1 で薬草を消費したら

//薬草由来の どうするメニュー、だれにメニュー は消さなければならない。

for( let i = menus.length - 1; i > 0; i -- ) {

let menu = menus[ i ];

let selfItem = menu.parent.selectedItem;

//親メニューに自身の選択項目がない。(薬草を使ったから薬草が消えた)

if( menu.parent.src.items.indexOf( selfItem ) == -1 ) {

menu.parent.removeChild( menu );

this.app.currentElement = menu.parent;

}

}

}

draw( cc ) {//m

super.draw( cc );

//ウィンドウとリストを描画


cc.save();


//原点を描画位置へ移動

let gx = this.style.col * this.style.cellSize;

let gy = this.style.row * this.style.cellSize;

cc.translate( gx, gy );


this.translateToItemArea( cc );


//カーソル

if( this.app.currentElement != this ||

this.app.metronomez.forCursor.toggle ) {

let w = this.style.fontSize * .7;

let h = this.style.fontSize * .8;

let gx = - this.style.fontSize + ( this.style.fontSize - w ) / 2;

let gy = this.cursorY * this.style.fontSize + ( this.style.fontSize - h ) / 2 + 1;

cc.beginPath();

cc.moveTo( gx, gy );

cc.lineTo( gx + w, gy + h / 2 );

cc.lineTo( gx, gy + h );

cc.closePath();

cc.fillStyle = "black";

cc.fill();

}


cc.restore();


super.drawChildren( cc );

}//draw()

}//Menu


class TitleScreen extends Element {//c

constructor( style, app ) {

super( style, app );

}

async typeKey( key ) {//m

this.app.artz.soundz.pushA.play();


this.app.removeChild( this );

this.app.mapScreen = new MapScreen( {

col : 0,

row : 0,

cols : this.app.style.cols + ( this.app.style.cols % 2 ? 0 : 1 ),

rows : this.app.style.rows + ( this.app.style.rows % 2 ? 0 : 1 ),

//偶数なら奇数にする

cellSize : 16,

}, this.app );

this.app.appendChild( this.app.mapScreen );


let map = this.app.status.player.position.map;

let x = this.app.status.player.position.x;

let y = this.app.status.player.position.y;

// this.app.mapScreen.playerMapMoveTo( map, x, y );

this.app.mapScreen.playerMapMoveTo( App.townmap, 3, 6 );


await this.app.tell( "スタート", { wait : true, close:true } );

}

draw( cc ) {//m

cc.fillStyle = "black";

cc.fillRect( 0, 0, cc.canvas.width, cc.canvas.height );

cc.fillStyle = "white";

cc.fillText( "hit any key", 100, 100 );

}

}//TitleScreen

// |

// V

class MapScreen extends Element {//c

constructor( style, app ) {

super( style, app );

this.actions = new Array();


this.colsHalf = Math.floor( this.style.cols / 2 );

this.rowsHalf = Math.floor( this.style.rows / 2 );

this.scroll = {

count : 0,

isAnimating : false,

tweakX : 0,

tweakY : 0,

}

this.keyLock = false;

}

// |

// V

tweakXForLooping( x ) {//m

//tweak. ループ時は範囲外の値を範囲内に調整する

if( this.cfg.maploop ) {

if( x < 0 )

x += this.mapWidth;

else if( x >= this.mapWidth )

x -= this.mapWidth;

}

return x;

}

tweakYForLooping( y ) {//m

//tweak. ループ時は範囲外の値を範囲内に調整する

if( this.cfg.maploop ) {

if( y < 0 )

y += this.mapHeight;

else if( y >= this.mapHeight )

y -= this.mapHeight;

}

return y;

}

getPositionAwayBy( step ) {//m

let dir = this.app.status.player.direction;

let x = this.sx + ( ( dir == 2 ) - ( dir == 0 ) ) * step;

let y = this.sy + ( ( dir == 3 ) - ( dir == 1 ) ) * step;

return { x : x, y : y }

}

// |

// V

typeKey( key ) {//m

if( key == 32 ) {

//check. 向いている方向に何かがある

let pos = this.getPositionAwayBy( 1 );

let bit;

if( bit = Utl.accessBy( this.mapBits, pos.y, pos.x ) ) {

if( bit == "□" ) {

pos = this.getPositionAwayBy( 2 );

this.actions.push( { x : pos.x, y : pos.y, type : "contact" } );

return;

}

}

if( bit = Utl.accessBy( this.storyBits, pos.y, pos.x ) ) {

if( bit.sprite ) {

this.actions.push( { x : pos.x, y : pos.y, type : "contact" } );

return;

}

}


//コマンドメニューを開く

this.app.artz.soundz.pushA.rapidPlay();


let campMenu = new Menu( "executemode", "camp", this.app.menusrcz.camp, {

col : 1,

row : 1,

}, this.app );

this.appendChild( campMenu );


}

}

// |

// V

senseKey( key ) {//m

//check.

if( this.scroll.isAnimating || this.keyLock ) return;


let addX, addY;


switch( key ) {

case 37: this.app.status.player.direction = 0; addX = -1; addY = 0; break;

case 38: this.app.status.player.direction = 1; addX = 0; addY = -1; break;

case 39: this.app.status.player.direction = 2; addX = 1; addY = 0; break;

case 40: this.app.status.player.direction = 3; addX = 0; addY = 1; break;

default: return;

}


//check. 移動先は壁

let sx = this.tweakXForLooping( this.sx + addX );

let sy = this.tweakYForLooping( this.sy + addY );

let bit;

if( bit = Utl.accessBy( this.mapBits, sy, sx ) )

if( this.cfg.walls.indexOf( bit ) > -1 ) {

this.app.artz.soundz.pushWall.play();

return;

}

if( Utl.accessBy( this.storyBits, sy, sx, "sprite" ) ) return;


//1マススクロールの起動

this.scroll.addX = addX;

this.scroll.addY = addY;

this.scroll.count = 0;

this.scroll.isAnimating = true;


}//senseKey

// |

// V

frameForScroll() {//m

//1マススクロールにおける、1ドット分のスクロール処理


this.scroll.tweakX -= this.scroll.addX;

this.scroll.tweakY -= this.scroll.addY;

//tweakX,Yが1ドットスクロールの主要のしくみ

//それについてはthis.draw()を参照してください。


this.scroll.count ++;

//check. スクロールの終了

if( this.scroll.count == this.style.cellSize ) {

this.scroll.isAnimating = false;


this.scroll.tweakX = 0;

this.scroll.tweakY = 0;

//draw()はスクロール終了後も参照するからここで0にする。


this.sx = this.tweakXForLooping( this.sx + this.scroll.addX );

this.sy = this.tweakYForLooping( this.sy + this.scroll.addY );


//マップ外に乗ったストーリー発生

if( ! this.cfg.maploop

&& ( this.sx < 0 || this.sx >= this.mapWidth

|| this.sy < 0 || this.sy >= this.mapHeight )

) {

this.actions.push( { x : -1, y : -1, type : "got_on" } );

} else {


//座標に乗ったストーリー発生

this.actions.push( { x : this.sx, y : this.sy, type : "got_on" } );

}


}//if スクロール終了


}//frameForScroll()

// |

// V

playerMapMoveTo( mapdatafunc /*other args*/ ) {//m


//マップ間移動


//check. 

if( this.app.bgm ) {

this.app.bgm.pause();

this.app.bgm.currentTime = 0;

}


//データを記述した関数からデータを取り出す1

let commentData = mapdatafunc.toString().match( /\/\*\r\n([\s\S]+)\r\n\*\// )[ 1 ];

//データを記述した関数からデータを取り出す2

this.cfg = mapdatafunc();


//取り出したデータを使えるように加工

this.mapBits = new Array();

this.storyBits = new Array();

this.stories = new Object();


//マップデータを読み取る ここから

let storyTmp = new Array();

let lines = commentData.split( /\r\n/ );


for( let y = 0; y < lines.length; y++ ) {

let line = lines[ y ];


this.mapBits[ y ] = new Array();

for( let x = 0; x < line.length; x++ ) {

let mapBit = line.substr( x, 1 );


//check. 半角文字のときはストーリーへのショートカット

if( mapBit.match( /[0-9a-z ]/i ) ) {

let shortcutId = mapBit + line.substr( x + 1, 1 );

shortcutId = shortcutId.replace( / /, "" );


//check. ショートカットがリンク切れ

if( ! this.cfg.shortcuts[ shortcutId ] ) {

alert( "マップ上に配置したショートカットがリンク切れ id:" + shortcutId );

continue;

}


//マップ上のショートカットについては後で処理

storyTmp.push( {

shortcutId : shortcutId,

x : x,

y : y,

} );

//ショートカットをマップデータに置き換え

mapBit = this.cfg.shortcuts[ shortcutId ].bit;


x++; //1文字多く読んだから

}


this.mapBits[ y ].push( mapBit );

}

}//for


this.mapWidth = this.mapBits[ 0 ].length;

this.mapHeight = this.mapBits.length;


//storyBits初期化

for( let y = 0; y < this.mapHeight; y++ ) {

this.storyBits[ y ] = new Array();

for( let x = 0; x < this.mapWidth; x++ ) {

this.storyBits[ y ][ x ] = null;

}

}

//storyBitsにstoryを配置

for( let tmp of storyTmp ) {


let shortcut = this.cfg.shortcuts[ tmp.shortcutId ];

this.storyBits[ tmp.y ][ tmp.x ] = shortcut;


//tweak. 座標情報をセット

shortcut.x = tmp.x;

shortcut.y = tmp.y;

}



//マップデータを読み取る ここまで



//BGM

if( this.cfg.bgmTitle ) {

this.app.bgm = this.app.artz.bgms[ this.cfg.bgmTitle ];

this.app.bgm.loop = true;

this.app.bgm.play();

}


//マップの外に出たときのストーリー

if( ! this.cfg.maploop ) {

let story = this.cfg.shortcuts.outer.story.got_on;

this.stories[ "outer,got_on" ] = story;

}


//メソッドの引数によって処理を分ける(オーバーロードみたいに)

let argtypes;

let argArray = Array.from( arguments );

argtypes = argArray.map( arg => ( typeof arg ).substr( 0, 1 ) ).join( "" );


//ショートカットを指定して移動

if( argtypes.indexOf( "fs" ) == 0 ) {


//playerMapMoveTo( function, string [,number] )

let shortcut = arguments[ 1 ];

this.sx = this.cfg.shortcuts[ shortcut ].x;

this.sy = this.cfg.shortcuts[ shortcut ].y;

//check.

if( typeof arguments[ 2 ] !== "undefined" )

this.app.status.player.direction = arguments[ 2 ];


} else if( argtypes.indexOf( "fnn" ) == 0 ) {


//移動先位置の座標指定して移動

//playerMapMoveTo( function, number, number [,number] )

this.sx = arguments[ 1 ];

this.sy = arguments[ 2 ];

//check.

if( typeof arguments[ 3 ] !== "undefined" )

this.app.status.player.direction = arguments[ 3 ];

} else {


alert( "playerMapMoveTo()の引数リストが想定外 : " + argtypes );


}


}//playerMapMoveTo()

// |

// V

playerMapMoveTo_fx( mapdatafunc /*other args*/ ) {//m


//視覚効果付きのマップ間移動


let argArray = Array.from( arguments );

//check. 歩く音

let donotFootsteps = false;

if( ( typeof argArray[ argArray.length - 1 ] ) == "boolean" ) {

donotFootsteps = argArray.pop();

}

if( ! donotFootsteps ) this.app.artz.soundz.walk.play();


this.keyLock = true;


//クロージャ(関数が終わっても、この関数内で定義した関数内では生き続ける変数)

let countMax = 3;

let count = countMax;

let flg = true;

//マップ間移動時の画面フェードアウト、イン

let metronome = this.app.addMetronome( "forFade", 125 );

this.app.anms.push( function() {

//check. メトロノームに合わせて

if( ! metronome.changed ) return;


if( flg ) {

//フェードアウト

count --;

let value = 100 * count / countMax;

this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";

//check. フェードアウトの終了

if( ! count ) {

//マップ間移動する

this.playerMapMoveTo( ...argArray );

flg = false;

count = countMax;

}

} else {

//フェードイン

count --;

let value = 100 * ( countMax - count ) / countMax;

this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";

//check. フェードインの終了(全終了)

if( ! count ) {

this.keyLock = false;

delete this.app.metronomez.forFade;

return true; //trueはアニメの終了の意

}

}

}.bind( this ) );

}//playerMapMoveTo_fx()


searchStory() {//m

let story = null;


for( let action of this.actions ) {

//ストーリー マップの外に出た

if( action.type == "got_on" && action.x == -1 && action.y == -1 ) {

story = this.stories[ "outer,got_on" ];

break;

}

//ストーリー その他

if( story = Utl.accessBy( this.storyBits, action.y, action.x, "story", action.type ) ) {

break;

}

}


this.actions.length = 0;

return story;

}


draw( cc ) {//m

cc.fillStyle = "black";

cc.fillRect( this.x, this.y, this.w, this.h );


//this.sx,this.syが画面中央に来るように描画開始座標を調整

let sx = this.sx - this.colsHalf + this.mapWidth;

let sy = this.sy - this.rowsHalf + this.mapHeight;

//+ this.mapWidth,Heightは、下の計算で % this.mapWidth,Height

//としたときに負値になるのを避けるために加えた


cc.save();

cc.translate(

-this.style.cellSize / 2 + this.scroll.tweakX,

-this.style.cellSize / 2 + this.scroll.tweakY

);

//tweakX,Yが描画位置を1ドットずつずらすので

//きれいに1ドットスクロールしてるようにみえる


cc.font = this.style.cellSize + "px ''";

cc.fillStyle = "rgb(0,255,0)";

for( let row = 0; row < this.style.rows; row++ ) {

let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に

let gy = row * this.style.cellSize;

for( let col = 0; col < this.style.cols; col++ ) {

let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に

let bit = this.mapBits[ y ][ x ];

let gx = col * this.style.cellSize;

let image = this.cfg.images[ bit ] ? this.cfg.images[ bit ] : bit;

cc.fillText( image, gx, gy + this.style.cellSize );


}

}



//スプライト描画

for( let row = 0; row < this.style.rows; row++ ) {

let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に

let gy = row * this.style.cellSize;

for( let col = 0; col < this.style.cols; col++ ) {

let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に

let story = this.storyBits[ y ][ x ];

//check.

if( ! story ) continue;

if( ! story.sprite ) continue;


let gx = col * this.style.cellSize;

let image = story.sprite;

cc.fillText( image, gx, gy + this.style.cellSize );

}

}


//debug. グリッド

if( 0 ) {

cc.strokeStyle = "rgba(255,255,255,.125)";

for( let row = 0; row < this.style.rows; row ++ ) {

for( let col = 0; col < this.style.cols; col ++ ) {

let x = col * this.style.cellSize;

let y = row * this.style.cellSize;

cc.strokeRect( x, y, this.style.cellSize, this.style.cellSize );

}

}

}


cc.restore();


//debug. 画面の中央線

if( 1 ) {

cc.strokeStyle = "red";

cc.beginPath();

cc.moveTo( 0, cc.canvas.height / 2 );

cc.lineTo( cc.canvas.width, cc.canvas.height / 2 );

cc.stroke();

cc.beginPath();

cc.moveTo( cc.canvas.width / 2, 0 );

cc.lineTo( cc.canvas.width / 2, cc.canvas.height );

cc.stroke();

}


//プレイヤーキャラ描画

cc.fillStyle = "cyan";

cc.font = this.style.cellSize + "px ''";

let gx = ( cc.canvas.width - this.style.cellSize ) / 2;

let gy = ( cc.canvas.height - this.style.cellSize ) / 2 + this.style.cellSize;

let sprite = this.app.artz.spritez.player;

let idx = this.app.status.player.direction * 2 + sprite.metronome.index;

let image = sprite.images[ idx ];

cc.fillText( image, gx, gy );



//子の描画(ウィンドウなど)

for( let child of this.childs ) child.draw( cc );

for( let float of this.floats ) float.draw( cc );


}//draw()

}//MapScreen


App.worldmap = function() {/*

●〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃¥¥¥▲〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山山山山〃~~~~~~¥¥〃〃〃〃〃〃〃〃〃¥¥山山山¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山山〃〃〃〃~~~〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥¥〃〃1 〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃山山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃山山山山山〃〃〃〃〃〃〃〃¥¥山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃山山山〃〃〃〃〃〃〃〃¥〃〃¥山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥〃〃〃〃山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥〃〃〃〃〃山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山山山〃〃〃~~~〃〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山山〃〃~~~~¥¥〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山〃〃~~~¥¥¥〃〃〃〃〃〃〃〃山山山山山山山〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃¥¥¥¥〃〃〃〃〃〃〃〃〃山山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃山山山山山山山山山山~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃山山山山山~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃¥¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃¥¥¥¥¥¥〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃¥¥¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃¥¥〃〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

*/

return {

maploop : true,

bgmTitle : "field",

walls : "~",

shortcuts : {

"1" : {

bit : "凸",

story : {

"got_on" : {

begin : function( e ) {

this.mapScreen.playerMapMoveTo_fx( App.townmap, "s", 2 );

},

},

},

},

},

images : {

"¥" : "🌲",

"凸" : "🏰",

"山" : "⛰",

"~" : "🌊",

},

}//return

}//worldmap



App.townmap = function() {/*

木木木木木木木〃・・〃木木木木木木木木木〃・・〃〃〃〃〃〃〃

木■■■■■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■刀刀刀■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■・ws・■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■■□■■木〃・・〃木■■■扉■■■木〃・・〃〃〃〃〃〃〃

木木〃・剣木木〃・・〃〃花花花・花花花〃〃・・〃〃〃〃〃〃〃

s ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・

・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・

木木盾・〃木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■■□■■木〃・・〃a1〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■・ss・■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■盾盾盾■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■■■■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木木木木木木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

*/

return {

maploop : false,

bgmTitle : "town",

walls : "■□扉",

shortcuts : {

outer : {

bit : "〃",

story : {

"got_on" : {

begin : function( e ) {

this.mapScreen.playerMapMoveTo_fx( App.worldmap, "1", 3 );

},

},

},

},

"s" : {

bit : "・",

},

"ws" : {

bit : "👨‍🔧",

story : {

"contact" : {

begin : async function( e ) {

await this.tell( "ここは刀剣屋だ。\n" );

await this.shop();

}

},

},

},

"a1" : {

bit : "〃",

sprite : "🕵️‍♂️",

story : {

"contact" : {

begin : async function( e ) {

await this.tell( "ドラゴンなら北のほうへ飛んで行ったぜ", { wait : true, close:true } );

}

},

},

},

"ss" : {

bit : "👨‍🔧",

},

},


images : {

"木" : "🌳",

"花" : "🌷",

"扉" : "🚪",

"箱" : "📦",

"座" : "🪑",

"寝" : "🛏",

"∪" : "🏺",

"剣" : "⚔",

"刀" : "🗡",

"盾" : "🛡",

},

}//return

}//townmap



現在コマンドメニューのプログラミングに時間がかかっています。

コマンドメニューと一言で言っても、

のようにいろいろな場面で使える、ザ・ウィンドウシステムになっています。(母音の前だからただしくはジ・ウィンドウシステム)

万能型で、なおかつ、簡潔に書きたいんですが、いまのところ一部が複雑なしくみになっていて、それをどのように簡潔にするのかが、ひとつの課題になっています。

現在はオブジェクト指向(クラスやクラス同士の継承)を使っていますが、将来的には BASIC でも実装できるようなプログラムにしたいと思っています。


(プログラムの著作権についてはあまり深く考えていませんが、もし使いたい場合は、自由に使ってください。試作品なので使えないは思いますが)


あこがれのプログラム

個人的なことですが、私の子供のころからあこがれていたプログラムは

  1. ファミコンみたいなスムーズスクロール
  2. BGM を鳴らす
  3. 3DCG プログラミング

の3点だったかなと思います。

このうち、1番の「スムーズスクロール」は、今回の試作品で実現されています。最近のパソコンとブラウザの処理スピード向上の恩恵かなと思います。あと JavaScript の requestAnimationFrame() を使用して vsync(ハードウェア的な描画信号)にあわせて描画することができるようになったことも大きいでしょう。とてもうれしいことです。

2番の「BGM を鳴らす」は今回の試作品でも鳴っていますが実は満足していません。音源が mp3 や wav なので、どうしても曲データの終わりで一度音が止まり最初から繰り返すという鳴り方になってしまいます。できれば楽譜のレベルでループで繰り返してほしいんです。この辺は同じ BGM をプログラム中に2つ用意して、2つの BGM をクロスフェードで重ねればある程度不満が軽減されるかなと思っています。または FM 音源の自作みたいなことも 難しいとは思いますが考えています。

3番の 3DCG は h = x * s / z、v = y * s / z の2式で実現できるので、満足しています。(今回の試作とは関係ありません)


今後は、戦闘システムを作ることになると思います。


(訪問者のどんなニーズと この記事がつながるか)


2021/7/22(木)

4連休とお盆休みの予定 -[life]

4連休 2021年7月22日(木)~2021年7月25日(日)

お盆休み 2021年8月7日(土)~2021年8月16日(火) 10日間


決めごと:


上記アンダーライン部分が普通じゃない記述です。その説明を…

読書の挿絵描画は可。 -[RPG]

最近、J.R.R.トールキンの「指輪物語」を読んでいて、なんとなく挿絵がほしくなって自分で描いて本に はさんでいます。

▼J.R.R.トールキンの「指輪物語」
トールキンはノーベル賞候補だった
▼P97付近
異変のうわさ
▼P101付近
ガンダルフ2度目の登場
▼P102
そしてフロドと指輪の脅威について話す

これを18時以降に可とします。でも1枚も描かないかもしれません。


ちなみに、この指輪物語は全部で10巻あり、amazon にて中古品で購入しました。


現在、1巻の 2/3 程度まで読んだところですが、ノーベル賞候補だったというのがよくわかります。↓


ただし、人によっては苦痛となりうるところもあるかも。↓

しかし、Wikipedia で調べてみるとその翻訳は、

訳 瀬田貞二 氏(1916年生まれ)

同氏は 1941 年に 東京 帝国大学 国文科 を卒業し、中学教師のかたわら児童文学作品を雑誌に寄稿し、1949年(訳者33才)に児童文学関係の出版社に入社、その後26年ほど児童文学の仕事にたずさわってきた。そして、

指輪物語の翻訳 1975年 訳者59才

ということなので、つまり、私が生まれて間もない1歳のときに、その道で活躍してきた60才くらいのご年配の方が洋書の翻訳をされたわけで、どこか日本寄りの古臭い表現になるのは、当然と言えば当然かもしれません。私が生まれて小学生くらいのころまで郵便ポストは赤い円筒形の陶器製だったし、自動販売機ではガラス瓶の1.5リットル コカ・コーラ(重い)が売られており、昔というのは確かに文化が違います。。鹿島立ち、昔の表現だからと言って悪く言うこともないでしょう。。

または、より正しい意味合いを、日本の人々に伝えるためには、日本の人々が親しんでいる言葉を使うのがベストだ、、という方針があったのかもしれません。

ともあれRPGのただ一点の祖先がD&Dであり、D&Dが作られるために必要だった「剣と魔法のファンタジー」という文化の上流にこの「指輪物語」があります。「指輪物語」を読むことで、RPG のエッセンスを垣間見ることができると思って読んでいます。


(私の「普通じゃない記述」について説明しているところでした)

「良いと思わなかった」と連続して記録を取り…

毎日決めて散歩していると、あきてきて、意味がないような気がするんです。

でもずっと部屋にいるよりは確かに身体には良いです。

なので、良いと思った、思わなかった、に関係なく散歩に毎日出かけたという結果がほしいと思って、毎日「良いと思わなかった」と記録を取ろう、と考えたわけです。あの手この手で自分をそっち方向へ向かわせたいんです。

もちろん良いに越したことはなく、良いと思ったら「良いと思った」と記録します。

4連休:

7月22日(木) 良いと思った
7月23日(金) 良いと思った
7月24日(土) 良いと思った
7月25日(日) 良いと思った

お盆休み:

8月7日(土)
8月8日(日)
8月9日(月)
8月10日(火)
8月11日(水)
8月12日(木)
8月13日(金)
8月14日(土)
8月15日(日)
8月16日(月)


なお、いつもなら勉強を予定するところですが、今回は今のところ予定せず、RPG開発 をなんとかしてひと段落させたいと考えています。


2021/7/21(水)

全体的・基本・簡潔のRPG

ページの最初の「NO PC WEEK に代わる PC 使用制限のしくみ(新β版)」の表で、下図のようにコマンドメニュー作成のパーセンテージを 0.1% ずつ微妙に進めている理由は、現在、プログラムを根本的に書き直しているからです。

  1. 「RPGのプログラム本体」(class App)、
  2. 「タイトル画面・マップ画面・コマンドメニュー画面」(class Screen)、
  3. 「コマンドメニューの各ウィンドウ」(class CmdElement)、

これら3種類の要素について、それぞれ別々にプログラムを書くのは普通の考え方ですが、それだとどうもプログラムが煩雑になって、行き詰ってしまうので、3種類の要素を1つの考え方で統一(3種類とも1つのクラスを継承)するように書き直しています。


(訪問者のどんなニーズと この記事がつながるか)


2021/7/18(日)

今月の冒頭スクリプト -[javascript]

今月は「よせてはかえす波」を文字のカタでくりぬいて表示、というスクリプトです。

前々から「文字そのものにゆらゆらとした波のアニメが表示される」、、というイメージが頭にあって、できそうな気がしてやってみました。


一応できましたが、ちょっと無理な方法で実現させたので、もしかしたら表示するブラウザ、パソコン、スマートホンによってはうまく表示できないこともあるかもしれません。


しくみ

  1. CSSの プロパティ background-clip と、-webkit-text-fill-color を使用して、文字部分に背景画像が表示されるようにします。
  2. 非表示状態の CANVAS に何かを描き、canvas.toDataURL() を使用して、描いた画面を文字列にし、HTML要素の style.backgroundImage に "url( その文字列 ) " の形で代入します。すると CANVAS の内容(静止画)を HTML要素の背景画像にできます。これを繰り返すことで静止画ではなくCANVASの描画の動き(アニメーション)をそのまま表示するようにします。
  3. 上記1番にて HTML 要素の背景が文字に表示されるように設定してあるので、文字部分に CANVAS の内容が表示されます。
  4. 上記2番の処理は CPU に負担をかけるので、IntersectionObserver を使って HTML 要素が見えるときだけ2番を行うようにします。
    (IntersectionObserver はちょっとだけ使い方が難しいです)


これだけならまだ良かったんですが、Firefox で描画内容がキャッシュされるまでのあいだ、文字部分がチラチラとちらつくので、事前にアニメーションのすべてのパターンを Image オブジェクトにして配列に格納しておくようにします。

その格納は無制限に格納するわけには やはりいかない(メモリーには限りがある)ので、行うアニメーションはコマ数を限定します。

アニメーションの動きを自由に値を加算して気ままに行うのではなく、アニメの開始から終了までの動きの量(回転の角度とか、物体の座標とか)を、コマ数で割って、、というような考え方になります。そうなると面倒くさいですよね。、、でもちょっと説明してみましょう。


donbura = 6.28 / this.counterMax * this.counter;

たとえば、物の回転は皆さんご存知の通り、0度から360度で一回りです。

プログラミングでは360度は、6.28 という数値を使って計算するので、6.28 で一回りです。(ちなみに6.28は円周率3.14の2倍です)

これは、その米(ごはん)が 1合なのか 180cc なのかという単位の違いと同じことなので、そんなに難しい話ではないはずです。

0.5合は 90cc という計算ですよね。同じことをここでもやります。

アニメで物を1回転させたいとき、6.28 が動きの全体量。そしてコマ数は限られていてその数は this.counterMax という変数に入っています。10回や100回などそういう数値が入っています。

全体量 6.28 を this.counterMax で割れば、「1コマ当たりの移動量」がわかります。

this.counterMax が 10 回ならば、6.28 / 10 で、「1コマ当たりの移動量」は 0.628 です。0.628 ずつ 10 回繰り返せば、6.28 になり、一回転となるわけです。

それでアニメが現在どのくらいまで進んだのかは、this.counter でカウントしていて、「1コマ当たりの移動量」に this.counter を掛け算すれば、現在のコマ数での回転の量がわかるわけです。

その回転の量を使って、何かを描けばそのコマを描くことができます。

気ままなアニメとは考え方が違うので、少し難しいかもしれませんね。

そういう感じで Firefox のためにアニメの考え方を変えて、対応したわけです。。が、


「Firefox はキャッシュを意図的に作ってあげれば ちらつく不具合が解消される」とは、誰も言っていなくて、私がそれで解決するだろうと勝手に思っただけです。

そのため、見ていると、Firefox は何かのタイミングでアニメのキャッシュ(そもそもそんなものがあるのだろうか??)を開放するらしく、プログラムのほうでアニメのコマ画像をいくら保持していても(Firefoxが開放してしまった際には)そんなの関係ないみたいです。ときどきちらつきます。

   

でも、
そんなのかんけーねぇ!
そんなのかんけーねぇ!

プログラムは途中で疲れてしまい、はやく完成させようと思って、殴り書きみたいなプログラムになってしまいました。

このページのどこでも白い部分を右クリックして、「ソースコードを表示」を選び(表示されるまで時間がかかるかも)、文字列検索で「wave」を検索すれば、当該のプログラムが出てきます。

プログラムを見ていくと、途中で class AnimationBGI {...} というクラス定義が出てきます。これはHTML要素の背景にCANVASの内容を適用するためのツールみたいなものです。が、今後使うかどうかはわかりません。


(訪問者のどんなニーズと この記事がつながるか)


2021/7/17(土)

JavaScript をページ内に表示して動かす -[javascript]

ホームページ上で JavaScript のゲームを遊んだり、面白い動きをする JavaScript プログラムを見るとき、画面の表示が小さいとつまらないですよね?

以前、私は WebBasic と称して、自動的に適切な大きさに拡大してくれる便利プログラムを JavaScript で自作していました。

スクロールしてプログラムの画面がウィンドウの外に出ると、プログラムの動きを止めて、CPUパワーを節約するという機能も搭載していました。

とても便利な感じがしていたんですが…


今日になって それらの便利な機能は、JavaScript の基本機能で実現できることがわかりました。(気づくのがおそい)

以下のプログラムは、ブラウザのウィンドウの大きさに合わせてページにうまくフィットする(横幅の65%の大きさにする)ように大きさが拡大され、ページをスクロールして画面が見えなくなると動きが止まります。


その JavaScript の基本機能を使ったプログラムはこんな感じです↓

拡大機能に関する部分は /*1*/、 スクロールで停止する機能に関する部分は/*2*/、

1つのページ内で複数のプログラムを掲載し動かす機能については /*3*/ で示しています。

拡大機能は、CSSの object-fit プロパティで実現し、スクロールで停止の機能は、JavaScript の IntersectionObserver で実現しています。

複数掲載については、個々のプログラムをそれぞれ class か、オブジェクトのいずれかにして、そのなかで完結するようにプログラムを作成します。それらをページ中の appz という配列(appz.app1のように名前で参照できるObject型の配列)におさめ、まぁ、上手に管理します。このアバウトな言い方が初心者の方には苦しいんだと思いますが、下のプログラムを参考にしてください。(時間がないので)

この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。


<div style="text-align:center;">

<canvas id="test012" width="256" height="224" style="

height : 80vh; /*1*/

image-rendering : crisp-edges;

object-fit : contain; /*1*/

background-color : gray;

border : solid 8px gray;

border-radius : 16px;

"></canvas>

</div><p>

<script>

class Test012 {

constructor() {

this.name = "test012";

this.cc = document.getElementById( this.name ).getContext( "2d", { alpha : false } );

this.t = 0;

}

frame() {

this.draw( this.cc );

this.t += 0.1;

this.timerId = requestAnimationFrame( this.frame.bind( this ) );

}

draw( cc ) {

cc.clearRect( 0, 0, cc.canvas.width, cc.canvas.height );

let r = Math.min( cc.canvas.width / 2, cc.canvas.height / 2 );


cc.beginPath();

cc.arc( cc.canvas.width / 2, cc.canvas.height / 2, r, this.t, this.t + 5 );

cc.strokeStyle = "#0F0";

cc.stroke();


cc.beginPath();

cc.arc( cc.canvas.width / 2, cc.canvas.height / 2, r - 10, -this.t - 1, -this.t - 5 );

cc.strokeStyle = "#F00";

cc.stroke();


cc.beginPath();

cc.arc( cc.canvas.width / 2, cc.canvas.height / 2, r - 20, this.t + 2, this.t + 5 );

cc.strokeStyle = "#00F";

cc.stroke();


}

start() { /*2*/

this.frame(); /*2*/

} /*2*/

stop() { /*2*/

cancelAnimationFrame( this.timerId ); /*2*/

this.timerId = null; /*2*/

} /*2*/

}


addEventListener( "load", function() { /*1, 2, 3*/

//check.

if( typeof appz === "undefined" ) { /*3*/

appz = new Object(); /*3*/

}

//check. 要素がユーザーの目に入ったかどうか /*2*/

if( typeof intersectionObserver === "undefined" ) { /*2*/

intersectionObserver = new IntersectionObserver( function( entries ) { /*2*/

for( let entry of entries ) { /*2*/

let app = appz[ entry.target.id ]; /*2*/

let bak = app.visible; /*2*/

app.visible = entry.intersectionRatio > 0; /*2*/

//check. /*2*/

if( bak == app.visible ) continue; /*2*/

if( app.visible ) { /*2*/

console.log( "app.start" ); /*2*/

app.start(); /*2*/

} else { /*2*/

console.log( "app.stop" ); /*2*/

app.stop(); /*2*/

} /*2*/

} /*2*/

}, { rootMargin : '-40% 0% -40% 0%', } ); /*2*/

} /*2*/


appz.test012 = new Test012(); /*3*/

intersectionObserver.observe( appz.test012.cc.canvas ); /*2*/

} );

</script>


作った WebBasic が水の泡!
骨折り損のくたびれ儲け
とは まさにこのことだ
フガッフガッ


複数のプログラムが動くという証明として、もう1つプログラムを掲載します↓

上のプログラムリストは HTML の一部の抜粋でしたが、以下のプログラムリストは HTML の全体です。この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。


<html><!--ESCAPEPROCESS-->

<head>

<meta content="text/html; charset=UTF-8" http-equiv="content-type">

<script>

console.clear();

</script>

</head>

<body>


<div style="text-align:center;">

<canvas id="test333" width="512" height="448" style="

width : 65%;

object-fit : contain;

"></canvas>

</div>


<script>

class test333 {

constructor() {

this.name = "test333";

this.cc = document.getElementById( this.name ).getContext( "2d", { alpha : true } );

this.t = 0;

}

preload() {

return new Promise( function( promiseOk ) {

this.image = new Image();

this.image.onload = promiseOk;

this.image.src = "ase_solid.png";

}.bind( this ) ).then( function() {

//promiseOk()実行後

this.w = 400;

this.h = this.image.height * ( this.w / this.image.width );

}.bind( this ) );

}

frame() {

this.draw( this.cc );

this.t += 0.025;

this.timerId = requestAnimationFrame( this.frame.bind( this ) );

}

draw( cc ) {

cc.clearRect( 0, 0, cc.canvas.width, cc.canvas.height );

cc.save();

cc.translate( cc.canvas.width / 2, cc.canvas.height / 2 );

cc.rotate( - this.t / 20 ); //全体の回転

let tweakX = this.w / 2 * 1.19;

let tweakY = this.h / 2 * 1.3;


//画像描画

cc.drawImage( this.image, - tweakX, - tweakY, this.w, this.h );

let a = this.w * 0.47 - tweakX; //右目の位置

let b = a + this.w * 0.25; //左目の位置

let y = this.h * 0.55 - tweakY; //両目の高さ

cc.strokeStyle = "black";

cc.lineWidth = this.w / 100;

//右目

cc.save();

cc.translate( a, y );

this.guruguru( cc, this.w / 10, this.t );

cc.restore();

//左目

cc.save();

cc.translate( b, y );

this.guruguru( cc, this.w / 10, this.t + 0.2 );

cc.restore();

//口

cc.save();

cc.translate( this.w * 0.6 - tweakX, this.h * 0.8 - tweakY );

cc.strokeStyle = "#e5a";

cc.fillStyle = "#f7c";

this.awawawa( cc, this.w / 8, this.t );

cc.restore();

cc.restore();


// cc.fillRect( cc.canvas.width / 2 - 2 , cc.canvas.height / 2 - 2, 4, 4 );

}

awawawa( cc, r, animateTheta ) {

//あわわわの口

let first = true;

cc.beginPath();

let tp = [ [ 0.2, 3 ], [ 0.2, 2 ], [ 0.2, 4 ], [ 0.5, 2 ] ][ 0 ];

for( let theta = 0, b = -30; theta < 6.2; theta += 0.1, b += 2 ) {

let arasa = tp[ 0 ]; //小さいほど荒い

let yura = tp[ 1 ]; //大きいほど荒い

let a = Math.sin( theta / arasa + animateTheta ) * yura; //円の半径をsinで伸縮

let x = ( Math.cos( theta ) * r + a ) * 1.2;

let y = ( Math.sin( theta ) * r + a ) / 2;

if( first ) {

cc.moveTo( x, y );

first = false;

} else

cc.lineTo( x, y );

}

cc.closePath();

cc.stroke();

cc.fill();

}

guruguru( cc, maxR, animateTheta ) {

//ぐるぐるの目

let first = true;

cc.beginPath();

for( let theta = 0, r = 0; r < maxR; theta += 0.1, r += this.w / 1700 ) {

let x = Math.cos( -theta + animateTheta ) * r;

let y = Math.sin( -theta + animateTheta ) * r;

if( first ) {

cc.moveTo( x, y );

first = false;

} else

cc.lineTo( x, y );


// cc.fillRect( x, y, 2, 2 );

}

cc.stroke();

}

start() {

this.frame();

}

stop() {

cancelAnimationFrame( this.timerId );

this.timerId = null;

}

}


addEventListener( "load", async function() {

//check.

if( typeof appz === "undefined" ) {

appz = new Object();

}

//check.

if( ! IntersectionObserver ) {

alert( "There is no 'IntersectionObserver'(API)." );

}

if( typeof intersectionObserver === "undefined" ) {

intersectionObserver = new IntersectionObserver( function( entries ) {

for( let entry of entries ) {

let app = appz[ entry.target.id ];

let bak = app.visible;

app.visible = entry.intersectionRatio > 0;

//check.

if( bak == app.visible ) continue;

if( app.visible ) {

console.log( "app.start" );

app.start();

} else {

console.log( "app.stop" );

app.stop();

}

}

}, {

// rootMargin : '-40% 0% -40% 0%',

} );

}


appz.test333 = new test333();

await appz.test333.preload();

await intersectionObserver.observe( appz.test333.cc.canvas );


} );

</script>

</body>

</html>


この↑HTMLファイル 使用している画像

このHTMLファイルと、使用している画像のリンクをそれぞれ右クリックして「名前を付けてリンク先を保存」等してダウンロードし、一緒のフォルダに入れて、HTMLファイルをインターネットブラウザで開けばお手元で動作すると思います。JavaScript プログラミングの勉強に役立ててください。


(訪問者のどんなニーズと この記事がつながるか)


2021/7/16(金)

SUPER PC WEEK -[no_pc]

2年前くらいから、このホームページ上で私自身のパソコンの使用時間について、「NO PC WEEK に代わる PC 使用制限のしくみ(新β版)」という表を使って管理し、パソコンの長時間の作業で身体を壊さないようにしています。

その追加の新しい仕組みとして「SUPER PC WEEK」というものを考えました。

これはパソコン使用時間の制限ではなく、「制限を無し」にして「好きなだけパソコンを使って良い」とするものです。


「NO PC WEEK」はそこそこうまくいっていて効果が出ています。その「NO PC WEEK」の一部の仕組みを使えば「好きなだけパソコンを使って良い」としても問題なさそうだと考えました。

また、連休中に行うものなので、仕事への影響も少ないと思います。

詳しくは、「NO PC WEEK に代わる PC 使用制限のしくみ(新β版)」の表の下部にあるタブの並びの左から3番目「例外事項」タブを見てください。


今日から3連休なので、「SUPER PC WEEK」にします。

おもに「全体的・基本・簡潔のRPG」の作業を行うつもりです。

(電子回路とか、電子機器組み立てとか、おざなりになっているのが少し気がかりですが)


(訪問者のどんなニーズと この記事がつながるか)


2021/7/11(日)

グリフォンモデリング -[modeling]

Shade3D は形状を直接ぼやぁっと光らせる機能がなく、「ソフトグロー」という材質設定を行った「別の物体」を重ねることで疑似的に光らせることができます。下図

グリフォンの翼で直接いろいろ試すのも良いんですが、レンダリングに時間がかかるので、この立方体のような単純な形で試作するのが良いです。

絵画を描く人も、プログラミングをする人も、モデリングをする人も、本番作品でいろいろ試さず、別途試作を作るのが効率がいいみたいです。

なお、Blenderだと簡単に光らせられるみたいです。これ(リンクはBingで画像検索)


試作と同じことをグリフォンでやろうとして、うまくいかず、今日は時間切れ。↓

ちょっと大変そうだけど、ちゃんとできるようになれば、表現力が上がるから頑張って行うことにします。

赤で発光させれば「燃えさかる」という表現もできそうです。

…できれば目玉の黒目も動かしたい…それもできれば表情豊かになるから。


全体的・基本・簡潔のRPG -[RPG,javascript]

自作RPGのスクリーンショットとプログラムを掲載していますが、動くものへのリンクはこの記事の中にありません。

コマンドメニュー部分のプログラミング 95 %の状態です。

前回からの変更点:

以下はその自作RPGのプログラムリストの全体です。(1520行あります)

この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。


/* memo.

名前のすみわけ

プログラム 物語 対策

----------------------------

イベント イベント 物語のイベント(シナリオスクリプト)をストーリーと呼ぶ

アイテム アイテム 物語のアイテム(どうぐ)をトレジャーと呼ぶ


その他の名前

配列変数の名前と、同じ内容の連想配列変数の名前について

たとえばキャラクターの配列と、同じキャラクターの連想配列の名前について

どちらも複数形の名前にするとcharactersになるので、

配列時 characters

連想配列時 characterz

とする。


spritesの読み方について

配列時 sprites スプライトス

連想配列時 spritez スプライトス

万一、プログラムについて話し合うときに「このスプライト ズ だけど…」というと

不自然なので、どちらもスプライト ス と読むことにする。

*/


class App {//c

constructor( canvas ) {

this.name = "app";


this.cc = canvas.getContext( "2d", { alpha : false } );

this.cc.canvas.width = 256;

this.cc.canvas.height = 224;


this.cellSize = 16;


//---メトロノーム

this.metronomez = new Object();

this.addMetronome( "t1000_i2" );

this.addMetronome( "t500_i2" );

this.addMetronome( "t250_i2" );

//カーソル専用(手動追加)

this.metronomez.forCursor = {

maxTime : 500,

maxIndex : 0,

time : 0,

index : 0,

toggle : true,

changed : false,

}

/*

メトロノームの各フラグの変化タイミング図解(タイミングチャート)

maxTime : 250,

maxIndex : 3, の場合の各フラグの動き↓


(経過時間) 0 ... 250 ... 500 ... 750 ... 1000 ...(ms)


time : 0, 0 ..249 0 ..249 0 ..249 0 ..249 0 .. ...(ms)


index : 0, 000000001111111122222222000000001111 ...


toggle : true, ~~~~~~~~________~~~~~~~~________~~~~ ...


changed : false, ________~_______~_______~_______~___ ...


~ : true

_ : false

... : 略

※このグラフ中の1文字単位の縦1列はrequestAnimationFrameに相当する


つまり、requestAnimationFrameごとに、

timeは経過時間を増やしていきmaxTimeになると0に戻る

indexはtimeがmaxTimeになるごとに1増え、maxIndexになると0に戻る

toggleはtimeがmaxTimeになるごとに真偽を反転する

changedはtimeがmaxTimeになったときだけ真になる


maxTimeでアニメのパラパラのスピード決め

toggleやchangedでアニメのパラパラの実行を判断

indexでアニメのパラパラの画の切り替え

*/


this.spritez = {

player : {

images : [ "←", "-", "↑", "|", "→", "-", "↓", "|" ],

metronome : this.metronomez.t500_i2,

},

}


this.anms = new Array();


this.artz = { //ex. resources

spritez : this.spritez,

bgms : new Object(),

soundz : new Object(),

}



this.screenz = {

title : new TitleScreen( this ),

map : new MapScreen( this ),

cmd : new CmdScreen( this ),

}


//各画面について

for( let name in this.screenz ) {

let screen = this.screenz[ name ];

//ショートカット

this[ name ] = screen;

}



this.characters = new Array();

this.characterz = new Object();

this.treasurez = new Object();



//---トレジャー

this.treasurez = {

"どうけん" : {


},

"せいどうけん" : {


},

"てっけん" : {


},

"こうてつけん" : {


},

"なんこう" : {

nextMenu : {

title : "どうする?",

items : [

{

name : "つかう",

title : "だれに?",

items : this.characters,

script : async function( cmd, char1, dougu, cmd2, char2 ) {

await this.tell( char1.name + "は" + char2.name + "に" + dougu.name + "を使った!", { close : true } );


let idx = char1[ cmd.name ].indexOf( dougu );

char1[ cmd.name ].splice( idx, 1 );

},

},

{

name : "わたす",

title : "だれに?",

items : this.characters,

script : async function( cmd, char1, dougu, cmd2, char2 ) {

await this.tell( char1.name + "は" + char2.name + "に" + dougu.name + "を渡した!", { close : true } );

},

},

{

name : "すてる",

script : async function( cmd, char1, dougu, cmd2 ) {

await this.tell( char1.name + "は" + dougu.name + "を捨てた!", { close : true } );


},

},

],//items

},//nextMenu

},//なんこう

}//treasurez


//tweak. キーを名前として要素内に登録

for( let key in this.treasurez ) {

this.treasurez[ key ].name = key;

}


//---キャラクター


this.characters.push( {

name : "キャラ1",

"どうぐ" : [

{ name : "つるぎ" },

this.treasurez[ "なんこう" ],

{ name : "つるぎ" },

],

} );


this.characters.push( {

name : "キャラ2",

"どうぐ" : [

{ name : "なんこう2" },

{ name : "つるぎ" },

],

} );

this.characters.push( {

name : "キャラ3",

"どうぐ" : [

{ name : "なんこう3" },

{ name : "つるぎ" },

],

} );

Utl.array2object( this.characters, this.characterz, element => element.name );


//---メニュー

this.menusrcs = [

{

name : "yesno",

title : "",

items : [

{

name : "はい",

},

{

name : "いいえ",

}

],

},

{

name : "camp",

title : "コマンド?",

items : [

{

name : "どうぐ",

title : "だれの?",

items : this.characters,

nextMenu : {

title : "どれ?",

row : 3,

},

},

{

name : "しらべる",

},

{

name : "まほう",

title : "だれの?",

items : this.characters,

},

],

},

]

this.menusrcz = Utl.objectFrom( this.menusrcs, element => element.name );



//素材のプリロード

return Promise.all( [

this.load( "bgm", "field", "_bgm/10 イルバーンズの遺跡 (遺跡・地上).wav" ),

this.load( "bgm", "town", "_bgm/1-05 街.wav" ),

this.load( "bgm", "battle", "_bgm/battle.wav" ),

this.load( "sound", "pushA", "_sound/pushA.mp3" ),

this.load( "sound", "pushWall", "_sound/pushWall.mp3" ),

this.load( "sound", "walk", "_sound/walk.wav" ),

] ).then( function() {

return this; //ここはコンストラクタなのでAppのインスタンスを返すこと

}.bind( this ) );


}//constructor()


addMetronome( name /*other args*/ ) {//m

//オーバーロード(メソッド名が同じで引数のパターンが異なる)の模造

//”関数の先頭で引数の型を判定する条件分岐で対応”

let argsId = "";

for( let arg of Array.from( arguments ) ) {

argsId += "," + ( typeof arg ).substr( 0, 1 );

}

let metronome;

switch( argsId ) {

case ",s" : //フォーマット名称 t時間_iインデックス上限

let tokens = name.split( /_/ );

metronome = {

maxTime : Number( tokens[ 0 ].substr( 1 ) ),

maxIndex : Number( tokens[ 1 ].substr( 1 ) ),

time : 0,

index : 0,

toggle : false,

changed : false,

}

this.metronomez[ name ] = metronome;

break;

case ",s,n" : //自由な名称、時間

let tm = arguments[ 1 ];

metronome = {

maxTime : tm,

maxIndex : 0,

time : 0,

index : 0,

toggle : false,

changed : false,

}

this.metronomez[ name ] = metronome;

break;

default:

alert( "addMetronome()にて、未定義の引数リスト:[" + args + "]" );

}

return metronome;


}//addMetronome()

async tell( message, flgs ) {//a

/* ストーリの記述で、いきなりmessageWindow.write()を行いたいとき、

messageWindowを未作成のとき、どうするか

という問題があるので、このtellメソッドは必要。

*/


//check. メッセージウィンドウがないときは作成する

if( ! this.messageWindow ) {

this.cmd.addElement( new MessageWindow( this ) );

}

//check. コマンド画面を有効にする

if( this.currentScreen != this.cmd ) {

this.currentScreen = this.cmd;

this.cmd.style.visibility = true;

}


await this.messageWindow.write( ...arguments );


}

async yesno( message, closeFlg ) {//m

await this.tell( message, { nokey : true } );


this.artz.soundz.pushA.rapidPlay();

this.cmd.style.visibility = true;

this.currentScreen = this.cmd;


let yesnoMenu = new Menu( this.menusrcz.yesno, 27, 24, this );

this.cmd.addElement( yesnoMenu, this.messageWindow );



return new Promise( function( endmark ) {

yesnoMenu.endmark = endmark;

}.bind( this ) ).then( function( selectedItem ) {

this.cmd.closeElement( yesnoMenu );

console.log( selectedItem );

if( selectedItem )

return selectedItem.name == "はい";

else

return false;

}.bind( this ) );

}

async shop() {//m

this.cmd.style.visibility = true;

this.currentScreen = this.cmd;


let menu = new Menu( {

name : "shop",

title : "刀剣屋 'ロバートウッドテイル'",

items : [

this.treasurez[ "どうけん" ],

this.treasurez[ "せいどうけん" ],

this.treasurez[ "てっけん" ],

this.treasurez[ "こうてつけん" ],

],

}, 2, 2, this );

this.cmd.addElement( menu );


while( 1 ) {

await this.tell( "何を買うんだ?\n", { nokey : true } );

let selectedItem = await new Promise( function( endmark ) {

menu.endmark = endmark;

}.bind( this ) );

//check.

if( ! selectedItem ) {

this.cmd.closeElement( menu );

break;

}

if( await this.yesno( selectedItem.name + "だな。\n" + "100エンだ。\n買うかね?\n" ) ) {

await this.tell( "まいどあり!\n\n", { nokey : true } );

}

this.cmd.currentElement = menu;

}

await this.tell( "ありがとうございました!", { close:true } );

this.currentScreen = this.map;

}


//プリロード

load( type, name, src ) {//m

return new Promise( function( endmark ) {

let object;

console.log( type, name, "loading.." );

switch( type ) {

case "bgm":

object = new Audio();

object.oncanplaythrough = function() {

this.artz.bgms[ name ] = object;

object.oncanplaythrough = null;

console.log( "\t", type, name, "loaded." );

endmark();

}.bind( this );

object.src = src;

object.load();

break;

case "sound":

object = new Audio();

object.oncanplaythrough = function() {

this.artz.soundz[ name ] = object;

object.oncanplaythrough = null;

console.log( "\t", type, name, "loaded." );

object.rapidPlay = function() {

object.pause();

object.currentTime = 0;

object.play();

/*

同じ音楽ファイルの連続演奏時は

最初にリセットをかける必要がある。

rapid=急速

*/

}

endmark();

}.bind( this );

object.src = src;

object.load();

break;

}

}.bind( this ) );

}//load()



async start() {//m

this.keys = new Array();


onkeydown = function( e ) {

if( this.keys.indexOf( e.which ) == -1 ) {

this.keys.push( e.which ); //キーセンス オン

this.currentScreen.typeKey( e.which, this );

//キータイプ 実行

}

}.bind( this );


onkeyup = function( e ) {

let idx = this.keys.indexOf( e.which );

if( idx > -1 ) this.keys[ idx ] *= -1;

//キーセンス オフ予定にする(同じ数の負値)

}.bind( this );


this.status = {

player : {

position : {

map : App.worldmap,

x : 0,

y : 0,

},

direction : 0,

},

}


//タイトル画面から始める

this.screenz.title.style.visibility = true;

this.currentScreen = this.screenz.title;


this.beforeTime = 0;


this.frame( 0 );


}//start()


frame( tm ) {//m

//経過時間

let diff = tm - this.beforeTime;

this.beforeTime = tm;


//メトロノーム書き換え

for( let key in this.metronomez ) {

let metronome = this.metronomez[ key ];

metronome.time += diff

//check. そのメトロノームは時を刻んだ

if( metronome.time >= metronome.maxTime ) {

metronome.toggle = ! metronome.toggle;

metronome.changed = true;

metronome.time = 0;

//check. インデックスを進める

metronome.index++;

if( metronome.index == metronome.maxIndex )

metronome.index = 0;

} else {

metronome.changed = false;

}

}


//キーセンス 処理

for( let i = this.keys.length - 1; i >= 0; i-- ) {

let key = this.keys[ i ];

this.currentScreen.senseKey( Math.abs( key ), this );

//キーセンス 実行


//check. キーセンスオフ予定ならキーセンス オフ

if( key < 0 ) this.keys.splice( i, 1 );

}


//1ドットスクロール実行

if( this.screenz.map.scroll.isAnimating ) {

this.screenz.map.frameForScroll();

}


//ストーリー検索&実行

let story;

if( story = this.currentScreen.searchStory() ) {

story.begin.call( this );

}


//アニメ処理

for( let i = this.anms.length - 1; i >= 0; i-- ) {

if( this.anms[ i ]() ) this.anms.splice( i, 1 );

}


this.draw( this.cc );


this.timerId = requestAnimationFrame( this.frame.bind( this ) );

}//frame()


draw( cc ) {//m

cc.clearRect( 0,0,cc.canvas.width, cc.canvas.height );

for( let screen of Object.values( this.screenz ) ) {

if( screen.style.visibility ) screen.draw( cc );

}

}


}//App


class Utl {//c

static accessBy( object /*keys*/ ) {//m

for( let i = 1; i < arguments.length; i++ ) {

object = object[ arguments[ i ] ];

if( object == null ) return null;

if( typeof object === "undefined" ) return undefined;

}

return object;

}

static array2object( array, object, keyMaker ) {//m

for( let element of array ) {

object[ keyMaker( element ) ] = element;

}

}


static objectFrom( array, keyMaker ) {//m

let object = new Object();

for( let element of array ) {

object[ keyMaker( element ) ] = element;

}

return object;

}

static maxLength( strArray ) {//m

let v = 0;

for( let str of strArray ) v = Math.max( v, str.length );

return v;

}

}


class CmdElement {//c

constructor( col, row, cols, rows, fontSize, app ) {

this.col = col;

this.row = row;

this.cols = cols;

this.rows = rows;

this.fontSize = fontSize;

this.app = app;


this.x = this.col * this.fontSize;

this.y = this.row * this.fontSize;

this.width = this.cols * this.fontSize;

this.height = this.rows * this.fontSize;

this.elementType = "cmdelement";

}

typeKey( key ) {//m

}

draw( cc ) {//m

//ウィンドウ

let x = this.x - this.fontSize / 2;

let y = this.y - this.fontSize / 2;

let w = this.width + this.fontSize;

let h = this.height + this.fontSize;


//ウィンドウ周囲に影

if( 1 ) {

let mv = 2;

let mh = 3;

cc.globalAlpha = 0.2;

cc.fillStyle = "black";

cc.fillRect( x - mh, y - mv, w + mh * 2, h + mv * 2 );

cc.globalAlpha = 1;

}


cc.fillStyle = "white";

cc.strokeStyle = "black";

cc.fillRect( x, y, w, h );

cc.strokeRect( x, y, w, h );

}

}


class Menu extends CmdElement {//c

constructor( src, col, row, app ) {

//1文字を単位としたときの行と列

let itemNames = src.items.map( item => item.name );

let cols = Utl.maxLength( itemNames.concat( src.title ) ) + 1;

let rows = src.items.length;

//check.

if( src.title ) rows++;


super( col, row, cols, rows, 8, app );

this.elementType = "menu";


this.src = src;


this.itemCols = 1;

this.itemRows = this.src.items.length;

this.cursorX = 0;

this.cursorY = 0;

}//Menu


typeKey( key ) {//m

let addX = ( key == 39 ) - ( key == 37 );

let addY = ( key == 40 ) - ( key == 38 );


if( addX || addY ) {

this.cursorX += addX;

this.cursorY += addY;

//check.

if( this.cursorY < 0 ) this.cursorY = 0;

if( this.cursorY >= this.itemRows ) this.cursorY = this.itemRows - 1;

//カーソル 動いた時点で「点滅の点灯開始状態」にする

//こうしないとチラチラして見づらい

this.app.metronomez.forCursor.toggle = true;

this.app.metronomez.forCursor.time = 0;

} else {

this.selectedItem = this.src.items[ this.cursorY ];

let col, row;

switch( key ) {

case 32://SPACE

this.app.artz.soundz.pushA.rapidPlay();


let menusrc;

//選択した項目はメニューである

if( this.selectedItem.items ) {

menusrc = this.selectedItem;

} else if( this.src.nextMenu ) {

//選択した項目はメニューではないが、

//this.src内にnextMenu指定がある。

menusrc = {

title : this.src.nextMenu.title, //どれ?

items : this.selectedItem[ this.src.name ], //その人[道具],

col : this.src.nextMenu.col,

row : this.src.nextMenu.row,

}

} else if( this.selectedItem.nextMenu ) {

//選択した項目はメニューではないが、

//this.selectedItem内にnextMenu指定がある。

menusrc = this.selectedItem.nextMenu;

} else {

//メニューの終了

this.app.cmd.execMenu();

if( this.endmark ) this.endmark( this.selectedItem );

return;

}


col = typeof menusrc.col === "undefined"

? ( this.col + this.selectedItem.name.length + 2 )

: menusrc.col;

row = typeof menusrc.row === "undefined"

? ( this.row + this.cursorY + 2 )

: menusrc.row;


let menu = new Menu( menusrc, col, row, this.app );

this.app.cmd.addElement( menu, this );


break;

case 66://B

if( this.endmark ) this.endmark( null );//ショップ等 ストーリー時

else this.app.cmd.closeElement( this ); //キャンプ等 非ストーリー時

break;

default:

console.log( key );

}//switch

}//if else

}//typeKey



draw( cc ) {//m


super.draw( cc );


cc.save();

cc.translate( this.x + this.fontSize, this.y );

cc.fillStyle = "black";

cc.font = this.fontSize + "px ''";



//タイトル

if( this.src.title ) {

cc.fillText( this.src.title, -this.fontSize * 0.3, this.fontSize * 0.65 );

cc.translate( 0, this.fontSize );

}


//項目

let i = 0;

for( let item of this.src.items ) {

let gx = 0;

let gy = ( i + 1 ) * this.fontSize;

cc.fillText( item.name, gx, gy );

i++;

}


//カーソル

if( this.app.cmd.currentElement != this ||

this.app.metronomez.forCursor.toggle ) {

let w = this.fontSize * .7;

let h = this.fontSize * .8;

let gx = - this.fontSize + ( this.fontSize - w ) / 2;

let gy = this.cursorY * this.fontSize + ( this.fontSize - h ) / 2 + 1;

cc.beginPath();

cc.moveTo( gx, gy );

cc.lineTo( gx + w, gy + h / 2 );

cc.lineTo( gx, gy + h );

cc.closePath();

cc.fillStyle = "black";

cc.fill();

}

cc.restore();

}

}


class MessageWindow extends CmdElement {//c

constructor( app ) {

super( 1, 13, 19, 5, 12, app );

this.elementType = "messagewindow";

this.matrix = new Array();

for( let r = 0; r < this.rows; r++ ) {

this.matrix.push( new Array() );

}

this.matrixCursorCol = 0;

this.matrixCursorRow = 0;

}

write( message, flgs ) {//m

//check.

if( ! flgs ) {

flgs = { nokey : false, close : false }

}


this.message = message;

this.flgs = flgs;


return new Promise( function( endmark ) {

//1文字ずつ表示するアニメーション

this.endmark = endmark;

this.metronome = this.app.addMetronome( "test1", 50 );

this.seek = 0;

this.app.anms.push( function() {


//check. メトロノームに合わせる

if( ! this.metronome.changed ) return;


let ch = this.message.substr( this.seek, 1 );

let kaigyo = false;


switch( ch ) {

case "\n": kaigyo = true; break;

default:

this.matrix[ this.matrixCursorRow ][ this.matrixCursorCol ] = ch;

this.matrixCursorCol ++;

//check. 右端

if( this.matrixCursorCol == this.cols ) kaigyo = true;

}//switch


//check. 改行

if( kaigyo ) {

this.matrixCursorCol = 0;

//check. 下端

if( this.matrixCursorRow < this.rows - 1 ) {

this.matrixCursorRow ++;

} else {

//1行スクロール

this.matrix.shift();

this.matrix.push( new Array() );

}

}


this.seek ++;

//check. メッセージ出力終了

if( this.seek == this.message.length ) {

if( this.flgs.nokey ) this.endmark();

return true;

}

}.bind( this ) );//anm

}.bind( this ) ).then( function() {

//endmark()されたらここに来る

if( this.flgs.close ) {

this.app.cmd.closeElement( this.app.messageWindow );

}

}.bind( this ) );//new Promise

}//write


typeKey( key ) {

if( this.seek < this.message.length ) {

//文字出力中のときは文字出力を一気に行う

this.seek = this.message.length - 1;

} else if( ! this.flgs.nokey ) {

//文字出力終了のときは、呼び出し元に処理終了を知らせる。

this.endmark();

}

}

draw( cc ) {//m

super.draw( cc );

cc.save();

cc.translate( this.x, this.y );


cc.font = this.fontSize + "px ''";

cc.fillStyle = "black";

for( let r = 0; r < this.rows; r++ ) {

let gy = this.fontSize * r;

for( let c = 0; c < this.cols; c++ ) {

let gx = this.fontSize * c;

let ch = this.matrix[ r ][ c ];

//check.

if( ! ch ) continue;

cc.fillText( ch, gx, gy + this.fontSize );

}

}


cc.restore();

}

}


class Screen {//c

constructor( app ) {//m

this.app = app;

this.actions = new Array();

this.style = {

visibility : false,

}

}

start() {

}

actionsPush() {//m

this.actions.push( Array.from( arguments ).join( "," ) );

}

searchStory() {//m


}


senseKey( key ) {

}

typeKey( key ) {

}

draw( cc ) {

}

}


class CmdScreen extends Screen {//c

constructor( app ) {//m

super( app );

this.style.visibility = false;

this.elements = new Array();

this.menus = new Array();

}

start( menusrc ) {//m

let menu = new Menu( menusrc, 1, 1, this.app );

this.app.cmd.addElement( menu );

}

addElement( element, parent ) {//m

this.elements.push( element );

this.currentElement = element;

//check. Menuならmenusにも登録

if( element instanceof Menu ) {

this.menus.push( element );

} else if( element instanceof MessageWindow ) {

this.app.messageWindow = element;

}

element.parent = parent;

}


closeElement( element ) {//m

this.elements.splice( this.elements.indexOf( element ), 1 );

//check. 物によっては他の場所でも削除

if( element instanceof Menu ) {

this.menus.splice( this.menus.indexOf( element ), 1 );

} else if( element instanceof MessageWindow ) {

delete this.app.messageWindow;

}


//削除後

if( element.parent ) {

//親の要素へ戻る

this.currentElement = element.parent;

} else if( this.elements.length ) {

//ひとつ前の要素へ戻る

this.currentElement = this.elements[ this.elements.length - 1 ];

} else {

//画面に何もなくなったら、マップ画面へ戻る

this.app.currentScreen = this.app.map;

}

}

async execMenu() {//m

let script;

//メニュー順に引数を配置する方式

let args = new Array();

for( let menu of this.menus ) {

if( menu.selectedItem.script ) script = menu.selectedItem.script;

args.push( menu.selectedItem );

}


if( script ) {

await script.call( this.app, ...args );


//check scriptによってサブメニューの元となったモノがなくなった。

for( let i = this.menus.length - 1; i > 0; i -- ) {

let menu = this.menus[ i ];

let selfItem = menu.parent.selectedItem;

//親メニューに自身の選択項目がない。(薬草を使ったから薬草が消えた)

if( menu.parent.src.items.indexOf( selfItem ) == -1 ) {

//自身にまつわるサブメニューを消去。(薬草をどうする等サブメニューを消去)

for( let j = this.menus.length - 1; j >= i; j-- ) {

this.closeElement( this.menus[ j ] );

}

}

}

}

}

typeKey( key ) {//m

this.currentElement.typeKey( key );

}

draw( cc ) {//m

for( let element of this.elements ) {

element.draw( cc );

}

cc.font = "10px ''";

cc.fillStyle = "red";

cc.fillText( this.currentElement.elementType, 0, 10 );

}

}//CmdScreen


class MapScreen extends Screen {//c

/*

マップの表示という意味ではなく、

フィールド画面の総合的処理という意味。

*/

constructor( app ) {//m

super( app );


this.cellSize = 16;

this.cols = 17;

this.rows = 15;

this.colsHalf = Math.floor( this.cols / 2 );

this.rowsHalf = Math.floor( this.rows / 2 );

this.scroll = {

count : 0,

isAnimating : false,

tweakX : 0,

tweakY : 0,

}

this.keyLock = false;


}//constructor

treatX( x ) {

//tweak. ループ時は範囲外の値を範囲内に調整する

if( this.cfg.maploop ) {

if( x < 0 )

x += this.mapWidth;

else if( x >= this.mapWidth )

x -= this.mapWidth;

}

return x;

}

treatY( y ) {

//tweak. ループ時は範囲外の値を範囲内に調整する

if( this.cfg.maploop ) {

if( y < 0 )

y += this.mapHeight;

else if( y >= this.mapHeight )

y -= this.mapHeight;

}

return y;

}

typeKey( key ) {//m

if( key == 32 ) {


//向いている方向に何かがある

let pos = this.getPositionBy( 1 );

let bit;

if( bit = Utl.accessBy( this.mapBits, pos.y, pos.x ) ) {

if( bit == "□" ) {

pos = this.getPositionBy( 2 );

this.actions.push( { x : pos.x, y : pos.y, type : "contact" } );

return;

} else {


}

}


this.app.artz.soundz.pushA.rapidPlay();

this.app.cmd.style.visibility = true;

this.app.currentScreen = this.app.cmd;


let menu = new Menu( this.app.menusrcz.camp, 1, 1, this.app );

this.app.cmd.addElement( menu );

}

}

searchStory() {//m

let story = null;


for( let action of this.actions ) {

//ストーリー マップの外に出た

if( action.type == "got_on" && action.x == -1 && action.y == -1 ) {

story = this.stories[ "outer,got_on" ];

break;

}

//ストーリー その他

if( story = Utl.accessBy( this.storyBits, action.y, action.x, "story", action.type ) ) break;

}


this.actions.length = 0;

return story;


}

getPositionBy( step ) {//m

let dir = this.app.status.player.direction;

let x = this.sx + ( ( dir == 2 ) - ( dir == 0 ) ) * step;

let y = this.sy + ( ( dir == 3 ) - ( dir == 1 ) ) * step;

return { x : x, y : y }

}

senseKey( key ) {//m

//check.

if( this.scroll.isAnimating || this.keyLock ) return;


let addX, addY;


switch( key ) {

case 37: this.app.status.player.direction = 0; addX = -1; addY = 0; break;

case 38: this.app.status.player.direction = 1; addX = 0; addY = -1; break;

case 39: this.app.status.player.direction = 2; addX = 1; addY = 0; break;

case 40: this.app.status.player.direction = 3; addX = 0; addY = 1; break;

default: return;

}


//check. 移動先は壁

let sx = this.treatX( this.sx + addX );

let sy = this.treatY( this.sy + addY );

let bit;

if( bit = Utl.accessBy( this.mapBits, sy, sx ) )

if( this.cfg.walls.indexOf( bit ) > -1 ) {

this.app.artz.soundz.pushWall.play();

return;

}


//1マススクロールの起動

this.scroll.addX = addX;

this.scroll.addY = addY;

this.scroll.count = 0;

this.scroll.isAnimating = true;


}//senseKey


frameForScroll() {//m

//1マススクロールにおける、1ドット分のスクロール処理


this.scroll.tweakX -= this.scroll.addX;

this.scroll.tweakY -= this.scroll.addY;

//tweakX,Yが1ドットスクロールの主要のしくみ

//それについてはthis.draw()を参照してください。


this.scroll.count ++;

//check. スクロールの終了

if( this.scroll.count == this.cellSize ) {

this.scroll.isAnimating = false;


this.scroll.tweakX = 0;

this.scroll.tweakY = 0;

//draw()はスクロール終了後も参照するからここで0にする。


this.sx = this.treatX( this.sx + this.scroll.addX );

this.sy = this.treatY( this.sy + this.scroll.addY );


//マップ外に乗ったストーリー発生

if( ! this.cfg.maploop

&& ( this.sx < 0 || this.sx >= this.mapWidth

|| this.sy < 0 || this.sy >= this.mapHeight )

) {

this.actions.push( { x : -1, y : -1, type : "got_on" } );

} else {


//座標に乗ったストーリー発生

this.actions.push( { x : this.sx, y : this.sy, type : "got_on" } );

}


}//if スクロール終了


}

playerMapMoveTo_fx( mapdatafunc /*other args*/ ) {//m


//視覚効果付きのマップ間移動


let argArray = Array.from( arguments );

//check. 歩く音

let donotFootsteps = false;

if( ( typeof argArray[ argArray.length - 1 ] ) == "boolean" ) {

donotFootsteps = argArray.pop();

}

if( ! donotFootsteps ) this.app.artz.soundz.walk.play();


this.keyLock = true;


//クロージャ(関数が終わっても、この関数内で定義した関数内では生き続ける変数)

let countMax = 3;

let count = countMax;

let flg = true;

//マップ間移動時の画面フェードアウト、イン

let metronome = this.app.addMetronome( "forFade", 125 );

this.app.anms.push( function() {

//check. メトロノームに合わせて

if( ! metronome.changed ) return;


if( flg ) {

//フェードアウト

count --;

let value = 100 * count / countMax;

this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";

//check. フェードアウトの終了

if( ! count ) {

//マップ間移動する

this.playerMapMoveTo( ...argArray );

flg = false;

count = countMax;

}

} else {

//フェードイン

count --;

let value = 100 * ( countMax - count ) / countMax;

this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";

//check. フェードインの終了(全終了)

if( ! count ) {

this.keyLock = false;

delete this.app.metronomez.forFade;

return true; //trueはアニメの終了の意

}

}

}.bind( this ) );

}

playerMapMoveTo( mapdatafunc /*other args*/ ) {//m


//マップ間移動


//check. 

if( this.app.bgm ) {

this.app.bgm.pause();

this.app.bgm.currentTime = 0;

}


//データを記述した関数からデータを取り出す1

let commentData = mapdatafunc.toString().match( /\/\*\r\n([\s\S]+)\r\n\*\// )[ 1 ];

//データを記述した関数からデータを取り出す2

this.cfg = mapdatafunc();


//取り出したデータを使えるように加工

this.mapBits = new Array();

this.storyBits = new Array();

this.stories = new Object();


//マップデータを読み取る ここから

let storyTmp = new Array();

let lines = commentData.split( /\r\n/ );


for( let y = 0; y < lines.length; y++ ) {

let line = lines[ y ];


this.mapBits[ y ] = new Array();

for( let x = 0; x < line.length; x++ ) {

let bgBit = line.substr( x, 1 );


//check. 半角文字のときはストーリーへのショートカット

if( bgBit.match( /[0-9a-z ]/i ) ) {

let shortcutId = bgBit + line.substr( x + 1, 1 );

shortcutId = shortcutId.replace( / /, "" );


//check. ショートカットがリンク切れ

if( ! this.cfg.shortcuts[ shortcutId ] ) {

alert( "マップ上に配置したショートカットがリンク切れ id:" + shortcutId );

continue;

}


//マップ上のショートカットについては後で処理

storyTmp.push( {

shortcutId : shortcutId,

x : x,

y : y,

} );

//ショートカットをマップデータに置き換え

bgBit = this.cfg.shortcuts[ shortcutId ].bit;


x++; //1文字多く読んだから

}


this.mapBits[ y ].push( bgBit );

}

}//for


this.mapWidth = this.mapBits[ 0 ].length;

this.mapHeight = this.mapBits.length;


//storyBits初期化

for( let y = 0; y < this.mapHeight; y++ ) {

this.storyBits[ y ] = new Array();

for( let x = 0; x < this.mapWidth; x++ ) {

this.storyBits[ y ][ x ] = null;

}

}

//storyBitsにstoryを配置

for( let tmp of storyTmp ) {


let shortcut = this.cfg.shortcuts[ tmp.shortcutId ];

this.storyBits[ tmp.y ][ tmp.x ] = shortcut;


//tweak. 座標情報をセット

shortcut.x = tmp.x;

shortcut.y = tmp.y;

}



//マップデータを読み取る ここまで



//BGM

if( this.cfg.bgmTitle ) {

this.app.bgm = this.app.artz.bgms[ this.cfg.bgmTitle ];

this.app.bgm.loop = true;

this.app.bgm.play();

}


//マップの外に出たときのストーリー

if( ! this.cfg.maploop ) {

let story = this.cfg.shortcuts.outer.story.got_on;

this.stories[ "outer,got_on" ] = story;

}


//メソッドの引数によって処理を分ける(オーバーロードみたいに)

let argtypes;

let argArray = Array.from( arguments );

argtypes = argArray.map( arg => ( typeof arg ).substr( 0, 1 ) ).join( "" );


//ショートカットを指定して移動

if( argtypes.indexOf( "fs" ) == 0 ) {


//playerMapMoveTo( function, string [,number] )

let shortcut = arguments[ 1 ];

this.sx = this.cfg.shortcuts[ shortcut ].x;

this.sy = this.cfg.shortcuts[ shortcut ].y;

//check.

if( typeof arguments[ 2 ] !== "undefined" )

this.app.status.player.direction = arguments[ 2 ];


} else if( argtypes.indexOf( "fnn" ) == 0 ) {


//移動先位置の座標指定して移動

//playerMapMoveTo( function, number, number [,number] )

this.sx = arguments[ 1 ];

this.sy = arguments[ 2 ];

//check.

if( typeof arguments[ 3 ] !== "undefined" )

this.app.status.player.direction = arguments[ 3 ];

} else {


alert( "playerMapMoveTo()の引数リストが想定外 : " + argtypes );


}


}//playerMapMoveTo

draw( cc ) {//m

cc.clearRect( 0, 0, 256, 224 );



//this.sx,this.syが画面中央に来るように描画開始座標を調整

let sx = this.sx - this.colsHalf + this.mapWidth;

let sy = this.sy - this.rowsHalf + this.mapHeight;

//+ this.mapWidth,Heightは、下の計算で % this.mapWidth,Height

//としたときに負値になるのを避けるため。


cc.save();

cc.translate( -this.cellSize / 2 + this.scroll.tweakX, -this.cellSize / 2 + this.scroll.tweakY );

//tweakX,Yが描画位置を1ドットずつずらすので

//きれいに1ドットスクロールしてるようにみえる


cc.font = this.cellSize + "px ''";

cc.fillStyle = "rgb(0,255,0)";

for( let row = 0; row < this.rows; row++ ) {

let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に

let gy = row * this.cellSize;

for( let col = 0; col < this.cols; col++ ) {

let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に

let bit = this.mapBits[ y ][ x ];

let gx = col * this.cellSize;

let image = this.cfg.images[ bit ] ? this.cfg.images[ bit ] : bit;

cc.fillText( image, gx, gy + this.cellSize );


}

}


//スプライト描画

for( let row = 0; row < this.rows; row++ ) {

let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に

let gy = row * this.cellSize;

for( let col = 0; col < this.cols; col++ ) {

let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に

let story = this.storyBits[ y ][ x ];

//check.

if( ! story ) continue;

if( ! story.sprite ) continue;


let gx = col * this.cellSize;

let image = story.sprite;

cc.fillText( image, gx, gy + this.cellSize );

}

}


cc.restore();


//プレイヤーキャラ描画

cc.fillStyle = "cyan";

cc.font = this.cellSize + "px ''";

let gx = ( cc.canvas.width - this.cellSize ) / 2;

let gy = ( cc.canvas.height - this.cellSize ) / 2 + this.cellSize;

let sprite = this.app.artz.spritez.player;

let idx = this.app.status.player.direction * 2 + sprite.metronome.index;

let image = sprite.images[ idx ];

cc.fillText( image, gx, gy );


}//draw

}//MapScreen


class TitleScreen extends Screen {//c

constructor( app ) {//m

super( app );

this.name = "title";

}

async typeKey( key ) {//m

this.app.artz.soundz.pushA.play();


//マップ画面を表示する。

let map = this.app.status.player.position.map;

let x = this.app.status.player.position.x;

let y = this.app.status.player.position.y;

// this.app.screenz.map.playerMapMoveTo( map, x, y );

this.app.screenz.map.playerMapMoveTo( App.townmap, 3, 6 );

this.app.currentScreen = this.app.screenz.map;

this.app.screenz.map.style.visibility = true;

this.style.visibility = false;


await this.app.tell( "スタート", { close:true } );

}

draw( cc ) {//m

cc.clearRect( 0, 0, cc.canvas.width, cc.canvas.height );

cc.fillStyle = "white";

cc.fillText( "hit any key", 100, 100 );

}

}


App.worldmap = function() {/*

●〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃¥¥¥▲〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山山山山〃~~~~~~¥¥〃〃〃〃〃〃〃〃〃¥¥山山山¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山山〃〃〃〃~~~〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥¥〃〃1 〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃山山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃山山山山山〃〃〃〃〃〃〃〃¥¥山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃山山山〃〃〃〃〃〃〃〃¥〃〃¥山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥〃〃〃〃山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥〃〃〃〃〃山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山山山〃〃〃~~~〃〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山山〃〃~~~~¥¥〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山〃〃~~~¥¥¥〃〃〃〃〃〃〃〃山山山山山山山〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃¥¥¥¥〃〃〃〃〃〃〃〃〃山山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃山山山山山山山山山山~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃山山山山山~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃¥¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃¥¥¥¥¥¥〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃¥¥¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃¥¥〃〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

*/

return {

maploop : true,

bgmTitle : "field",

walls : "~",

shortcuts : {

"1" : {

bit : "凸",

story : {

"got_on" : {

begin : function( e ) {

this.screenz.map.playerMapMoveTo_fx( App.townmap, "s", 2 );

},

},

},

},

},

images : {

"¥" : "🌲",

"凸" : "🏰",

"山" : "⛰",

"~" : "🌊",

},

}//return

}//worldmap



App.townmap = function() {/*

木木木木木木木〃・・〃木木木木木木木木木〃・・〃〃〃〃〃〃〃

木■■■■■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■刀刀刀■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■・ws・■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■■□■■木〃・・〃木■■■扉■■■木〃・・〃〃〃〃〃〃〃

木木〃・剣木木〃・・〃〃花花花・花花花〃〃・・〃〃〃〃〃〃〃

s ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・

・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・

木木盾・〃木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■■□■■木〃・・〃a1〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■・ss・■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■盾盾盾■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■■■■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木木木木木木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

*/

return {

maploop : false,

bgmTitle : "town",

walls : "■□扉",

shortcuts : {

outer : {

bit : "〃",

story : {

"got_on" : {

begin : function( e ) {

this.screenz.map.playerMapMoveTo_fx( App.worldmap, "1", 3 );

},

},

},

},

"s" : {

bit : "・",

},

"ws" : {

bit : "👨‍🔧",

story : {

"contact" : {

begin : async function( e ) {

await this.tell( "ここは刀剣屋だ。\n" );

await this.shop();

}

},

},

},

"a1" : {

bit : "〃",

sprite : "🕵️‍♂️",

story : {

"contact" : {

begin : function( e ) {

this.tell( "ドラゴンなら北のほうへ飛んで行ったぜ", true );

}

},

},

},

"ss" : {

bit : "👨‍🔧",

},

},


images : {

"木" : "🌳",

"花" : "🌷",

"扉" : "🚪",

"箱" : "📦",

"座" : "🪑",

"寝" : "🛏",

"∪" : "🏺",

"剣" : "⚔",

"刀" : "🗡",

"盾" : "🛡",

},

}//return

}//townmap


※プログラムリスト中の小さな画像は「絵文字」です。絵文字がRPGのキャラの即席の代用に使えると思って利用しています。


(訪問者のどんなニーズと この記事がつながるか)


2021/7/3(土)

全体的・基本・簡潔のRPG -[RPG,javascript]

最近私は、「全体的・基本・簡潔のRPG」という名目で 自作のロールプレイングゲームを作っています。

昔からの趣味で、RPG (ロールプレイングゲーム)について その部品みたいなプログラム(マップを表示するだけ、キャラが上下左右に動くだけ、戦闘画面だけ等)をいろいろ作ってきましたが、30年目にしてやっと、部品ではなくて全体的なものを作り始めた感じです。

RPG開発の「原点」または「ひな形」、「テンプレート」となるように、ごく基本的で簡潔なプログラムで作ろうとしています。

現在は、「コマンドメニュー作成85%」の状態です。プログラム言語は JavaScript。

このスクリーンショットではコマンドメニューを展開しているところが写されています。

コマンドメニューについては以前 研究していたので、わりと つまづかずにプログラムできています。

RPGってプログラムとしては だいぶ複雑だなぁ、と感じています。

高校生のときの自分には無理だったんじゃないか、と思います。


その高校生の自分には無理だった、というプログラムリストがこれ↓

この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。


class App {//c

constructor( canvas ) {

this.name = "app";


this.cc = canvas.getContext( "2d", { alpha : false } );

this.cc.canvas.width = 256;

this.cc.canvas.height = 224;


this.cellSize = 16;


//---メトロノーム

this.metronomes = new Object();

this.addMetronome( "t1000_i2" );

this.addMetronome( "t500_i2" );

this.addMetronome( "t250_i2" );

//カーソル専用(手動追加)

this.metronomes.forCursor = {

maxTime : 500,

maxIndex : 0,

time : 0,

index : 0,

toggle : true,

changed : false,

}

/*

メトロノームの各フラグの変化タイミング図解(タイミングチャート)

maxTime : 250,

maxIndex : 3, の場合の各フラグの動き↓


(経過時間) 0 ... 250 ... 500 ... 750 ... 1000 ...(ms)


time : 0, 0 ..249 0 ..249 0 ..249 0 ..249 0 .. ...(ms)


index : 0, 000000001111111122222222000000001111 ...


toggle : true, ~~~~~~~~________~~~~~~~~________~~~~ ...


changed : false, ________~_______~_______~_______~___ ...


~ : true

_ : false

... : 略

※このグラフ中の1文字単位の縦1列はrequestAnimationFrameに相当する


つまり、requestAnimationFrameごとに、

timeは経過時間を増やしていきmaxTimeになると0に戻る

indexはtimeがmaxTimeになるごとに1増え、maxIndexになると0に戻る

toggleはtimeがmaxTimeになるごとに真偽を反転する

changedはtimeがmaxTimeになったときだけ真になる


maxTimeでアニメのパラパラのスピード決め

toggleやchangedでアニメのパラパラの実行を判断

indexでアニメのパラパラの画の切り替え

*/


this.sprites = {

player : {

images : [ "🦸‍♂️", "🦸‍♂️" ],

// images : [ "🙍", "🙎" ],

metronome : this.metronomes.t500_i2,

},

}


this.anms = new Array();

this.artz = { //ex. resources

sprites : this.sprites,

bgms : new Object(),

soundz : new Object(),

}



this.screenz = {

title : new TitleScreen( this ),

map : new MapScreen( this ),

cmd : new CmdScreen( this ),

}


//各画面について

for( let name in this.screenz ) {

let screen = this.screenz[ name ];

//メソッドのthisをappにする

// this.screenz[ name ] = this.changeForApp( screen );

//ショートカット

this[ name ] = screen;

}


//---キャラクター

this.characters = new Array();


this.characters.push( {

name : "キャラ1",

"どうぐ" : [

{

name : "なんこう1",

nextMenu : {

title : "どうする?",

items : [

{

name : "つかう",

title : "だれに?",

items : this.characters,

script : async function( cmd, char1, dougu, cmd2, char2 ) {

await this.typeMessage( char1.name + "は" + char2.name + "に" + dougu.name + "を使った!" );

},

},

{

name : "わたす",

title : "だれに?",

items : this.characters,

script : function( cmd, char1, dougu, char2 ) {

console.log( char1.name + "は" + char2.name + "に" + dougu.name + "をわたした" );

},

},

{

name : "すてる",

script : function( cmd, char1, dougu, char2 ) {

console.log( char1.name + "は" + dougu.name + "を捨てた!" );

},

},

],

},

},

{ name : "つるぎ" },

],

} );


this.characters.push( {

name : "キャラ2",

"どうぐ" : [

{ name : "なんこう2" },

{ name : "つるぎ" },

],

} );

this.characters.push( {

name : "キャラ3",

"どうぐ" : [

{ name : "なんこう3" },

{ name : "つるぎ" },

],

} );

this.characterz = Utl.objectFrom( this.characters, element => element.name );


//---メニュー

this.menusrcs = [

{

name : "camp",

title : "コマンド?",

items : [

{

name : "どうぐ",

title : "だれの?",

items : this.characters,

nextMenu : {

title : "どれ?",

row : 3,

},

},

{

name : "しらべる",

},

{

name : "まほう",

title : "だれの?",

items : this.characters,

},

],

},

]

this.menusrcz = Utl.objectFrom( this.menusrcs, element => element.name );


//---ストーリー

//(プログラムのイベントとの競合を避けるため、ゲームのシナリオイベントのことをストーリーと呼んでいる)

this.screenz.title.sysStories = {

"selected_start" : { //タイトル画面でスタートが選ばれたら

begin : function() {

//マップ画面を表示する。

let map = this.status.position.map;

let x = this.status.player.position.x;

let y = this.status.player.position.y;

this.screenz.map.playerMapMoveTo( map, x, y );

this.currentScreen = this.screenz.map;

this.screenz.map.style.visibility = true;

this.screenz.title.style.visibility = false;

},

},

}

this.screenz.map.sysStories = {

"pushA" : {

begin : function() {

//コマンド画面を表示する。

this.currentScreen = this.screenz.cmd;

this.screenz.cmd.style.visibility = true;

},

},

}

this.screenz.cmd.sysStories = {

"pushB" : {

begin : function() {

//マップ画面を表示する。

this.currentScreen = this.screenz.map;

this.screenz.cmd.style.visibility = false;

},

},

}


//素材のプリロード

return Promise.all( [

this.load( "bgm", "field", "_bgm/field.wav" ),

this.load( "bgm", "town", "_bgm/town.wav" ),

this.load( "bgm", "battle", "_bgm/battle.wav" ),

this.load( "sound", "pushA", "_sound/pushA.mp3" ),

this.load( "sound", "pushWall", "_sound/pushWall.mp3" ),

this.load( "sound", "walk", "_sound/walk.wav" ),

] ).then( function() {

return this; //ここはコンストラクタなのでAppのインスタンスを返すこと

}.bind( this ) );

}//constructor()


addMetronome( name /*other args*/ ) {//m

//オーバーロード(メソッド名が同じで引数のパターンが異なる)の模造

//”関数の先頭で引数の型を判定する条件分岐で対応”

let argsId = "";

for( let arg of Array.from( arguments ) ) {

argsId += "," + ( typeof arg ).substr( 0, 1 );

}

let metronome;

switch( argsId ) {

case ",s" : //フォーマット名称 t時間_iインデックス上限

let tokens = name.split( /_/ );

metronome = {

maxTime : Number( tokens[ 0 ].substr( 1 ) ),

maxIndex : Number( tokens[ 1 ].substr( 1 ) ),

time : 0,

index : 0,

toggle : false,

changed : false,

}

this.metronomes[ name ] = metronome;

break;

case ",s,n" : //自由な名称、時間

let tm = arguments[ 1 ];

metronome = {

maxTime : tm,

maxIndex : 0,

time : 0,

index : 0,

toggle : false,

changed : false,

}

this.metronomes[ name ] = metronome;

break;

default:

alert( "addMetronome()にて、未定義の引数リスト:[" + args + "]" );

}

return metronome;


}//addMetronome()


typeMessage( message ) {//m

//check. メッセージウィンドウがないときは作成する

if( ! this.cmd.messageWindow ) {

this.cmd.messageWindow = new MessageWindow( this );

this.cmd.addElement( this.cmd.messageWindow );

}

//check. コマンド画面を有効にする

if( this.currentScreen != this.cmd ) {

this.currentScreen = this.cmd;

this.cmd.style.visibility = true;

}


let mw = this.cmd.messageWindow;


return new Promise( function( aizu ) {

let metronome = this.addMetronome( "test1", 50 );

mw.write( message, metronome, aizu );

}.bind( this ) ).then( function() {

this.cmd.closeElement( mw );

delete this.cmd.messageWindow;

}.bind( this ) );

}


//プリロード

load( type, name, src ) {//m

return new Promise( function( promiseOK ) {

let object;

console.log( type, name, "loading.." );

switch( type ) {

case "bgm":

object = new Audio();

object.oncanplaythrough = function() {

this.artz.bgms[ name ] = object;

object.oncanplaythrough = null;

console.log( "\t", type, name, "loaded." );

promiseOK();

}.bind( this );

object.src = src;

object.load();

break;

case "sound":

object = new Audio();

object.oncanplaythrough = function() {

this.artz.soundz[ name ] = object;

object.oncanplaythrough = null;

console.log( "\t", type, name, "loaded." );

object.rapidPlay = function() {

object.pause();

object.currentTime = 0;

object.play();

/*

同じ音楽ファイルの連続演奏時は

最初にリセットをかける必要がある。

rapid=急速

*/

}

promiseOK();

}.bind( this );

object.src = src;

object.load();

break;

}

}.bind( this ) );

}//load()



async start() {//m

this.keys = new Array();


onkeydown = function( e ) {

if( this.keys.indexOf( e.which ) == -1 ) {

this.keys.push( e.which ); //キーセンス オン

this.currentScreen.typeKey( e.which, this );

//キータイプ 実行

}

}.bind( this );


onkeyup = function( e ) {

let idx = this.keys.indexOf( e.which );

if( idx > -1 ) this.keys[ idx ] *= -1;

//キーセンス オフ予定にする(同じ数の負値)

}.bind( this );


this.status = {

player : {

position : {

map : App.worldmap,

x : 0,

y : 0,

},

},

}


//タイトル画面から始める

this.screenz.title.style.visibility = true;

this.currentScreen = this.screenz.title;


this.beforeTime = 0;


this.frame( 0 );


}//start()


frame( tm ) {//m

//経過時間

let diff = tm - this.beforeTime;

this.beforeTime = tm;


//メトロノーム書き換え

for( let key in this.metronomes ) {

let metronome = this.metronomes[ key ];

metronome.time += diff

//check. そのメトロノームは時を刻んだ

if( metronome.time >= metronome.maxTime ) {

metronome.toggle = ! metronome.toggle;

metronome.changed = true;

metronome.time = 0;

//check. インデックスを進める

metronome.index++;

if( metronome.index == metronome.maxIndex )

metronome.index = 0;

} else {

metronome.changed = false;

}

}


//キーセンス 処理

for( let i = this.keys.length - 1; i >= 0; i-- ) {

let key = this.keys[ i ];

this.currentScreen.senseKey( Math.abs( key ), this );

//キーセンス 実行


//check. キーセンスオフ予定ならキーセンス オフ

if( key < 0 ) this.keys.splice( i, 1 );

}


//1ドットスクロール実行

if( this.screenz.map.scroll.isAnimating ) {

this.screenz.map.frameForScroll();

}


//ストーリー検索&実行

let story;

if( story = this.currentScreen.searchStory() ) {

story.begin.call( this );

}


//アニメ処理

for( let i = this.anms.length - 1; i >= 0; i-- ) {

if( this.anms[ i ]() ) this.anms.splice( i, 1 );

}


this.draw( this.cc );


this.timerId = requestAnimationFrame( this.frame.bind( this ) );

}//frame()


draw( cc ) {//m

cc.clearRect( 0,0,cc.canvas.width, cc.canvas.height );

for( let screen of Object.values( this.screenz ) ) {

if( screen.style.visibility ) screen.draw( cc );

}

}


}//App


class Utl {//c

static objectFrom( array, keyMaker ) {//m

let object = new Object();

for( let element of array ) {

object[ keyMaker( element ) ] = element;

}

return object;

}

static maxLength( strArray ) {//m

let v = 0;

for( let str of strArray ) v = Math.max( v, str.length );

return v;

}

}


class CmdElement {

constructor( col, row, cols, rows, fontSize, app ) {

this.col = col;

this.row = row;

this.cols = cols;

this.rows = rows;

this.fontSize = fontSize;

this.app = app;


this.x = this.col * this.fontSize;

this.y = this.row * this.fontSize;

this.width = this.cols * this.fontSize;

this.height = this.rows * this.fontSize;

}

typeKey( key ) {

}

draw( cc ) {

//ウィンドウ

cc.fillStyle = "white";

cc.strokeStyle = "black";

let x = this.x - this.fontSize / 2;

let y = this.y - this.fontSize / 2;

let w = this.width + this.fontSize;

let h = this.height + this.fontSize;

cc.fillRect( x, y, w, h );

cc.strokeRect( x, y, w, h );

}

}


class Menu extends CmdElement {//c

constructor( src, col, row, app ) {

//1文字を単位としたときの行と列

let itemNames = src.items.map( item => item.name );

let cols = Utl.maxLength( itemNames.concat( src.title ) ) + 1;

let rows = src.items.length + 1;


super( col, row, cols, rows, 8, app );


this.src = src;


this.itemCols = 1;

this.itemRows = this.src.items.length;

this.cursorX = 0;

this.cursorY = 0;



}//Menu


typeKey( key ) {//m

let addX = ( key == 39 ) - ( key == 37 );

let addY = ( key == 40 ) - ( key == 38 );


if( addX || addY ) {

this.cursorX += addX;

this.cursorY += addY;

//check.

if( this.cursorY < 0 ) this.cursorY = 0;

if( this.cursorY >= this.itemRows ) this.cursorY = this.itemRows - 1;

//カーソル 動いた時点で「点滅の点灯開始状態」にする

//こうしないとチラチラして見づらい

this.app.metronomes.forCursor.toggle = true;

this.app.metronomes.forCursor.time = 0;

} else {

this.selectedItem = this.src.items[ this.cursorY ];

let col, row;

switch( key ) {

case 32://SPACE

this.app.artz.soundz.pushA.rapidPlay();


let menusrc;

//選択した項目はメニューである

if( this.selectedItem.items ) {

menusrc = this.selectedItem;

} else {

//選択した項目はメニューではない

//しかし、nextMenu指定がある(2か所のいずれか)

if( this.src.nextMenu ) {

//this.srcにnextMenu

menusrc = {

title : this.src.nextMenu.title, //どれ?

items : this.selectedItem[ this.src.name ], //その人[道具],

col : this.src.nextMenu.col,

row : this.src.nextMenu.row,

}

} else if( this.selectedItem.nextMenu ) {

//this.selectedItemにnextMenu

menusrc = this.selectedItem.nextMenu;

}

}

//次のメニューが決定された

if( menusrc ) {

col = typeof menusrc.col === "undefined"

? ( this.col + this.selectedItem.name.length + 2 )

: menusrc.col;

row = typeof menusrc.row === "undefined"

? ( this.row + this.cursorY + 2 )

: menusrc.row;

let menu = new Menu( menusrc, col, row, this.app );

this.app.cmd.addElement( menu );

} else {

//メニューの終了

this.app.cmd.execMenu();

}


break;

case 27://ESC

this.app.cmd.closeElement( this );

break;

default:

console.log( key );

}//switch

}//if else

}//typeKey



draw( cc ) {//m


super.draw( cc );


cc.save();

cc.translate( this.x + this.fontSize, this.y );


//タイトル

cc.font = this.fontSize + "px ''";

cc.fillStyle = "black";

cc.fillText( this.src.title, -this.fontSize * 0.3, this.fontSize * 0.65 );


//項目

let i = 0;

for( let item of this.src.items ) {

let gx = 0;

let gy = ( i + 2 ) * this.fontSize;

cc.fillText( item.name, gx, gy );

i++;

}


//カーソル

if( this.app.cmd.currentElement != this ||

this.app.metronomes.forCursor.toggle ) {

let w = this.fontSize * .7;

let h = this.fontSize * .8;

let gx = - this.fontSize + ( this.fontSize - w ) / 2;

let gy = ( this.cursorY + 1 ) * this.fontSize + ( this.fontSize - h ) / 2 + 1;

cc.beginPath();

cc.moveTo( gx, gy );

cc.lineTo( gx + w, gy + h / 2 );

cc.lineTo( gx, gy + h );

cc.closePath();

cc.fillStyle = "black";

cc.fill();

}

cc.restore();

}

}


class MessageWindow extends CmdElement {//c

constructor( app ) {

super( 1, 13, 19, 5, 12, app );

}

write( message, metronome, aizu ) {//m

this.message = message;

this.metronome = metronome;

this.aizu = aizu;


this.seek = 0;

this.app.anms.push( function() {

//check.

if( ! this.metronome.changed ) return;

this.seek ++;

//check.

if( this.seek == this.message.length ) {

return true;

}

}.bind( this ) );

}

typeKey( key ) {

if( this.seek < this.message.length ) {

//文字出力中のときは文字出力を一気に行う

this.seek = this.message.length - 1;

} else {

//文字出力終了のときは、呼び出し元に処理終了を知らせる。

this.aizu();

}

}

draw( cc ) {//m

super.draw( cc );

cc.save();

cc.translate( this.x, this.y );


cc.font = this.fontSize + "px ''";

cc.fillStyle = "black";

cc.fillText( this.message.substr( 0, this.seek ), 0, this.fontSize );

cc.restore();

}

}


class Screen {//c

constructor( app ) {//m

this.app = app;

this.actions = new Array();

this.style = {

visibility : false,

}

}

start() {

}

actionsPush() {//m

this.actions.push( Array.from( arguments ).join( "," ) );

}

searchStory() {//m

let story = null;

for( let i = this.actions.length - 1; i >= 0; i-- ) {

let action = this.actions[ i ];


if( story = this.stories[ action ] ) break;

if( story = this.sysStories[ action ] ) break;


}

this.actions.length = 0;

return story;

}


senseKey( key ) {

}

typeKey( key ) {

}

draw( cc ) {

}

}


class CmdScreen extends Screen {//c

constructor( app ) {//m

super( app );

this.style.visibility = false;

this.elements = new Array();

this.menus = new Array();

}

start( menusrc ) {//m

this.topMenu = this.openMenu( menusrc, 1, 1 );

}

addMenu( menu ) {

this.menus.push( menu );

this.addElement( menu );

}

addElement( element ) {//m

this.elements.push( element );

this.currentElement = element;

//check.

if( element instanceof Menu ) {

this.menus.push( element );

}

}

hitAnyKey() {

return new Promise( function( aizu ) {

let onkeydown_bak = onkeydown;

let onkeyup_bak = onkeyup;

onkeydown = function( e ) {

onkeydown = onkeydown_bak;

onkeyup = onkeyup_bak;

aizu();

}

}.bind( this ) );

}

openMessage( message ) {//m

return new Promise( function( aizu ) {

let hukidasi = new Hukidasi();

let metronome = this.app.addMetronome( "test1", 50 );

hukidasi.tell( message, aizu, metronome );

this.app.anms.push( hukidasi.anm.bind( hukidasi ) );

hukidasi.parent = this.currentMenu;

this.addElement( hukidasi );

this.currentMenu = hukidasi;

}.bind( this ) );

}

openMenu( menusrc, col, row ) {//m

let menu = new Menu( menusrc, col, row, this.app )

menu.parent = this.currentMenu;

this.addMenu( menu );

this.currentMenu = menu;

return menu;

}

execMenu() {//m

let script;

//メニュー順に引数を配置する方式

let args = new Array();

for( let menu of this.menus ) {

if( menu.selectedItem.script ) script = menu.selectedItem.script;

args.push( menu.selectedItem );

}

script.call( this.app, ...args );

}

closeElement( element ) {//m

this.elements.splice( this.elements.indexOf( element ), 1 );

//check.

if( element instanceof Menu ) {

this.menus.splice( this.menus.indexOf( element ), 1 );

}


if( this.elements.length ) {

this.currentElement = this.elements[ this.elements.length - 1 ];

} else {

this.app.currentScreen = this.app.map;

}

}

typeKey( key ) {//m

this.currentElement.typeKey( key );

}

draw( cc ) {//m

for( let element of this.elements ) {

element.draw( cc );

}

}

}


class MapScreen extends Screen {//c

/*

マップの表示という意味ではなく、

フィールド画面の総合的処理という意味。

*/

constructor( app ) {//m

super( app );


this.cellSize = 16;

this.cols = 17;

this.rows = 15;

this.colsHalf = Math.floor( this.cols / 2 );

this.rowsHalf = Math.floor( this.rows / 2 );

this.scroll = {

count : 0,

isAnimating : false,

tweakX : 0,

tweakY : 0,

}

this.keyLock = false;


}//constructor

treatX( x ) {

//tweak. ループ時は範囲外の値を範囲内に調整する

if( this.cfg.maploop ) {

if( x < 0 )

x += this.mapWidth;

else if( x >= this.mapWidth )

x -= this.mapWidth;

}

return x;

}

treatY( y ) {

//tweak. ループ時は範囲外の値を範囲内に調整する

if( this.cfg.maploop ) {

if( y < 0 )

y += this.mapHeight;

else if( y >= this.mapHeight )

y -= this.mapHeight;

}

return y;

}

typeKey( key ) {//m

if( key == 32 ) {

this.app.artz.soundz.pushA.rapidPlay();

this.app.cmd.style.visibility = true;

this.app.currentScreen = this.app.cmd;


let menu = new Menu( this.app.menusrcz.camp, 1, 1, this.app );

this.app.cmd.addElement( menu );

}

}

senseKey( key ) {//m

//check.

if( this.scroll.isAnimating || this.keyLock ) return;


let addX = ( key == 39 ) - ( key == 37 );

let addY = ( key == 40 ) - ( key == 38 );


//check. 移動先は壁

let sx = this.treatX( this.sx + addX );

let sy = this.treatY( this.sy + addY );

if( this.mapBits[ sy ] )

if( this.mapBits[ sy ][ sx ] )

if( this.cfg.walls.indexOf( this.mapBits[ sy ][ sx ] ) > -1 ) {

this.app.artz.soundz.pushWall.play();

return;

}


//1マススクロールの起動

if( addX || addY ) {

this.scroll.addX = addX;

this.scroll.addY = addY;

this.scroll.count = 0;

this.scroll.isAnimating = true;

}//if 一歩移動


}//senseKey


frameForScroll() {//m

//1マススクロールにおける、1ドット分のスクロール処理


this.scroll.tweakX -= this.scroll.addX;

this.scroll.tweakY -= this.scroll.addY;

//tweakX,Yが1ドットスクロールの主要のしくみ

//それについてはthis.draw()を参照してください。


this.scroll.count ++;

//check. スクロールの終了

if( this.scroll.count == this.cellSize ) {

this.scroll.isAnimating = false;


this.scroll.tweakX = 0;

this.scroll.tweakY = 0;

//draw()はスクロール終了後も参照するからここで0にする。


this.sx = this.treatX( this.sx + this.scroll.addX );

this.sy = this.treatY( this.sy + this.scroll.addY );


//マップ外に乗ったストーリー発生

if( ! this.cfg.maploop

&& ( this.sx < 0 || this.sx >= this.mapWidth

|| this.sy < 0 || this.sy >= this.mapHeight )

) {

this.actionsPush( "outer,got_on" );

} else {


//座標に乗ったストーリー発生

this.actionsPush( this.sx, this.sy, "got_on" );

}


}//if スクロール終了


}

playerMapMoveTo_fx( mapdatafunc /*other args*/ ) {//m


//視覚効果付きのマップ間移動


let argArray = Array.from( arguments );

//check. 歩く音

let donotFootsteps = false;

if( ( typeof argArray[ argArray.length - 1 ] ) == "boolean" ) {

donotFootsteps = argArray.pop();

}

if( ! donotFootsteps ) this.app.artz.soundz.walk.play();


this.keyLock = true;


//クロージャ(関数が終わっても、この関数内で定義した関数内では生き続ける変数)

let countMax = 3;

let count = countMax;

let flg = true;

//マップ間移動時の画面フェードアウト、イン

let metronome = this.app.addMetronome( "forFade", 125 );

this.app.anms.push( function() {

//check. メトロノームに合わせて

if( ! metronome.changed ) return;


if( flg ) {

//フェードアウト

count --;

let value = 100 * count / countMax;

this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";

//check. フェードアウトの終了

if( ! count ) {

//マップ間移動する

this.playerMapMoveTo( ...argArray );

flg = false;

count = countMax;

}

} else {

//フェードイン

count --;

let value = 100 * ( countMax - count ) / countMax;

this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";

//check. フェードインの終了(全終了)

if( ! count ) {

this.keyLock = false;

delete this.app.metronomes.forFade;

return true; //trueはアニメの終了の意

}

}

}.bind( this ) );

}

playerMapMoveTo( mapdatafunc /*other args*/ ) {//m


//マップ間移動


//check. 

if( this.app.bgm ) {

this.app.bgm.pause();

this.app.bgm.currentTime = 0;

}


//データを記述した関数からデータを取り出す1

let commentData = mapdatafunc.toString().match( /\/\*\r\n([\s\S]+)\r\n\*\// )[ 1 ];

//データを記述した関数からデータを取り出す2

this.cfg = mapdatafunc();


//取り出したデータを使えるように加工

this.mapBits = new Array();

this.stories = new Object();


//マップデータを読み取る ここから

let lines = commentData.split( /\r\n/ );


for( let y = 0; y < lines.length; y++ ) {

let line = lines[ y ];

let bits = new Array();

for( let x = 0; x < line.length; x++ ) {

let bit = line.substr( x, 1 );

//check. 半角文字のときはストーリー設定あり

if( bit.match( /[0-9a-z ]/i ) ) {

let id = bit + line.substr( x + 1, 1 );

id = id.replace( / /, "" );

//check. エラー

if( ! this.cfg.shortcuts[ id ] ) {

alert( "マップ上に配置されたストーリー識別子が、ストーリー定義されていません。id:" + id );

bits.push( "!" );

continue;

}


//作業:ストーリーを座標情報をつけて複写

let stories = this.cfg.shortcuts[ id ].stories;

for( let key in stories ) {

let newkey = [ x, y, key ].join( "," );

this.stories[ newkey ] = stories[ key ];

}


//作業:半角文字をマップデータに直す

bit = this.cfg.shortcuts[ id ].bit;


//作業:座標情報をセット

this.cfg.shortcuts[ id ].x = x;

this.cfg.shortcuts[ id ].y = y;


x++;

}

bits.push( bit );

}

this.mapBits.push( bits );

}//for

//マップデータを読み取る ここまで


//BGM

if( this.cfg.bgmTitle ) {

this.app.bgm = this.app.artz.bgms[ this.cfg.bgmTitle ];

this.app.bgm.loop = true;

this.app.bgm.play();

}


//マップの外に出たときのストーリー

if( ! this.cfg.maploop ) {

let story = this.cfg.shortcuts.outer.stories.got_on;

this.stories[ "outer,got_on" ] = story;

}


//メソッドの引数によって処理を分ける(オーバーロードみたいに)

let argtypes;

let argArray = Array.from( arguments );

argtypes = argArray.map( arg => ( typeof arg ).substr( 0, 1 ) ).join( "" );


if( argtypes == "fs" ) {

//playerMapMoveTo( function, string )時

//移動先位置のショートカット指定

let shortcut = arguments[ 1 ];

this.sx = this.cfg.shortcuts[ shortcut ].x;

this.sy = this.cfg.shortcuts[ shortcut ].y;

} else {

//playerMapMoveTo( function, number, number )時

//移動先位置の座標指定

this.sx = arguments[ 1 ];

this.sy = arguments[ 2 ];

}


this.mapWidth = this.mapBits[ 0 ].length;

this.mapHeight = this.mapBits.length;


this.debugStories = new Object();

Object.keys( this.stories ).map( function( key ) {

let tokens = key.split( /,/ );

let x = tokens[ 0 ];

let y = tokens[ 1 ];

//check.

if( ! this.debugStories[ y ] )

this.debugStories[ y ] = new Object();

this.debugStories[ y ][ x ] = true;

}.bind( this ) );


}//playerMapMoveTo

draw( cc ) {//m

cc.clearRect( 0, 0, 256, 224 );



//this.sx,this.syが画面中央に来るように描画開始座標を調整

let sx = this.sx - this.colsHalf + this.mapWidth;

let sy = this.sy - this.rowsHalf + this.mapHeight;

//+ this.mapWidth,Heightは、下の計算で % this.mapWidth,Height

//としたときに負値になるのを避けるため。


cc.save();

cc.translate( -this.cellSize / 2 + this.scroll.tweakX, -this.cellSize / 2 + this.scroll.tweakY );

//tweakX,Yが描画位置を1ドットずつずらすので

//きれいに1ドットスクロールしてるようにみえる


cc.font = this.cellSize + "px ''";

cc.fillStyle = "rgb(0,255,0)";

for( let row = 0; row < this.rows; row++ ) {

let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に

let gy = row * this.cellSize;

for( let col = 0; col < this.cols; col++ ) {

let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に

let bit = this.mapBits[ y ][ x ];

let gx = col * this.cellSize;

let image = this.cfg.images[ bit ] ? this.cfg.images[ bit ] : bit;

cc.fillText( image, gx, gy + this.cellSize );


//debug.

if( 1 )

if( this.debugStories[ y ] )

if( this.debugStories[ y ][ x ] ) {

cc.strokeStyle = "red";

cc.strokeRect( gx, gy, this.cellSize, this.cellSize );

}

}

}

cc.restore();


//プレイヤーキャラ描画

cc.fillStyle = "red";

cc.font = this.cellSize + "px ''";

let gx = ( cc.canvas.width - this.cellSize ) / 2;

let gy = ( cc.canvas.height - this.cellSize ) / 2 + this.cellSize;

let sprite = this.app.artz.sprites.player;

let image = sprite.images[ sprite.metronome.index ];

cc.fillText( image, gx, gy );


}//draw

}//MapScreen


class TitleScreen extends Screen {//c

constructor( app ) {//m

super( app );

this.name = "title";

}

async typeKey( key ) {//m

this.app.artz.soundz.pushA.play();


//マップ画面を表示する。

let map = this.app.status.player.position.map;

let x = this.app.status.player.position.x;

let y = this.app.status.player.position.y;

this.app.screenz.map.playerMapMoveTo( map, x, y );

this.app.currentScreen = this.app.screenz.map;

this.app.screenz.map.style.visibility = true;

this.style.visibility = false;


await this.app.typeMessage( "スタート" );

}

draw( cc ) {//m

cc.clearRect( 0, 0, cc.canvas.width, cc.canvas.height );

cc.fillStyle = "white";

cc.fillText( "hit any key", 100, 100 );

}

}


App.worldmap = function() {/*

●〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃¥¥¥▲〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山山山山〃~~~~~~¥¥〃〃〃〃〃〃〃〃〃¥¥山山山¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山山〃〃〃〃~~~〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

¥¥〃〃1 〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃山山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃山山山山山〃〃〃〃〃〃〃〃¥¥山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃山山山〃〃〃〃〃〃〃〃¥〃〃¥山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥〃〃〃〃山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥〃〃〃〃〃山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山山山〃〃〃~~~〃〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山山〃〃~~~~¥¥〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

山〃〃~~~¥¥¥〃〃〃〃〃〃〃〃山山山山山山山〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃¥¥¥¥〃〃〃〃〃〃〃〃〃山山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃山山山山山山山山山山~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃山山山山山~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃¥¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃¥¥¥¥¥¥〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃¥¥¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃¥¥〃〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山

*/

return {

maploop : true,

bgmTitle : "field",

walls : "~",

shortcuts : {

"1" : {

bit : "凸",

stories : {

"got_on" : {

begin : function( e ) {

this.screenz.map.playerMapMoveTo_fx( App.townmap, "s" );

},

},

},

},

},

images : {

"¥" : "🌲",

"凸" : "🏰",

"山" : "⛰",

"~" : "🌊",

},

}//return

}//worldmap



App.townmap = function() {/*

木木木木木木木〃・・〃木木木木木木木木木〃・・〃〃〃〃〃〃〃

木■■■■■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■刀刀刀■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■・ws・■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃

木■■□■■木〃・・〃木■■■扉■■■木〃・・〃〃〃〃〃〃〃

木木〃・剣木木〃・・〃〃花花花・花花花〃〃・・〃〃〃〃〃〃〃

s ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・

・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・

木木盾・〃木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■■□■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■・ss・■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■盾盾盾■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木■■■■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

木木木木木木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃

*/

return {

maploop : false,

bgmTitle : "town",

walls : "■□扉",

shortcuts : {

outer : {

bit : "〃",

stories : {

"got_on" : {

begin : function( e ) {

this.screenz.map.playerMapMoveTo_fx( App.worldmap, "1" );

},

},

},

},

"s" : {

bit : "・",

},

"ws" : {

bit : "👨‍🔧",

},

"ss" : {

bit : "👨‍🔧",

},

},

images : {

"木" : "🌳",

"花" : "🌷",

"扉" : "🚪",

"箱" : "📦",

"座" : "🪑",

"寝" : "🛏",

"∪" : "🏺",

"剣" : "⚔",

"刀" : "🗡",

"盾" : "🛡",

},

}//return

}//townmap



どうしてそんなに RPG なのか?

現実から離れた疑似体験が面白いんだと思います。

知らない世界を歩き、剣や魔法などの装備を整え、想像上のモンスターに出会い戦う。

数々の謎が行く手をはばみ、解き明かすと新しい展開に驚かされる。


もちろん、アクションゲームやシューティングゲームなど他のゲームも同じことが当てはまります。

自機を操りステージを縦横無尽に進むのは 現実から離れた疑似体験だし、武器を整えることもあるし、敵もいろいろ出てくる。

ステージの最後まで行きつくとボスキャラがいてその大きさや攻撃に驚かされ、ボスを倒せば次のステージのまた違った新しい様子に驚かされます。


でも RPG というのは細かいコマンドでさまざまなことができるので、より現実の生活に近いことができ、世界に気持ちを移しやすいのかもしれません。

なんといっても、ムード満点のBGMを流しながら、世界を歩く様子は、ドラマを感じずにはいられません。(以下の動画)


 MP4ファイル/00:01:25/22.6MB

この動画にはファルコムのゲームソフトの楽曲が使われていますが、著作権については以下のようになっています。

  1. ファルコム音楽フリー宣言
    動画のBGMに使用するとか、催し物のBGMとして流すとか、そういう用途に限って、日本ファルコムが販売したすべての楽曲を利用してよいというものです。
  2. 同宣言、楽曲利用規約 第3条(コピーライト) より
    ”楽曲の利用に際しては画像、映像、印刷物、パンフレット、掲示物、ホームページ等に以下のコピーライトを表記するものとします。”
    → 動画中に表記しました。
  3. 同宣言、楽曲利用規約 第4条(禁止事項) より
    "有償又は無償を問わず、ゲームコンテンツに利用する行為。" (を禁止します)
    → これはゲームではなく動画です
      (なお、ゲームを動く状態で公開する際は、同楽曲は外して公開します)

でも、まるで、「規約に違反し、公開ゲームソフトに利用している」かのように見えるので、誤解を招きそうではあります。

実際、JavaScript のプログラムリストの中で同楽曲のファイルを指定して演奏するようになっています。

しかし、それをゲームソフトとして公開していないので、規約の意図には違反していないと考えています。

(上のプログラムリストでは、公開時の著作権の問題を考えてBGMのファイル名を差し替えています)

我ながら微妙なことしてるな~


ちなみに動画中の画面スクロールは撮影の関係でぎこちなくなっていますが、実際はファミコンのようなスムーズスクロールになっています。


(訪問者のどんなニーズと この記事がつながるか)